前言 多線程一直是初學者最困惑的地方,每次看到一篇文章,覺得很有難度,就馬上叉掉,不看了,我以前也是這樣過來的。後來,我發現這樣的態度不行,知難而退,永遠進步不了。於是,我狠下心來看完別人的博客,儘管很難但還是咬著牙,不懂去查閱資料,到最後弄懂整個過程。雖然花費時間很大,但這就是自學的精髓,別人學不會,而我卻學到了。很簡單的一個例子,一開始我對自定義View也是很抵觸,看到很難的圖就不去思考他,故意避開它,然而當我看到自己喜歡的雷達圖時,很有興趣的去查閱資料,不知不覺,自定義View對我已經沒有難度了。所以對於多線程我也是0基礎,不過我還是咬著牙皮,該學的還是得學。這裡先總結這幾個類特點和區別,讓大家帶著模糊印象來學習這篇文章 - Thread是個線程,而且有自己的生命週期
- 對於線程常用的操作有:wait(等待)、notify(喚醒)、notifyAll、sleep(睡眠)、join(阻塞)、yield(禮讓)
- wait、notify、notifyAll都必須在synchronized中執行,否則會拋出異常
- synchronized關鍵字和ReentrantLock鎖都是輔助線程同步使用的
- 初學者常犯的誤區:一個對像只有一個鎖(正確的)
本篇文章包含以下內容 線程同步之synchronized關鍵字馬上就過年了,火車搶票又是一年沸沸揚揚的事情,這也就好比我們的多線程搶奪資源是一個道理,下面我們通過火車搶票的案例來理解 - public class SyncActivity extends AppCompatActivity {
- private int ticket = 10;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_sync);
- for (int i = 0; i < 10; i++) {
- new Thread() {
- @Override
- public void run() {
- //買票
- sellTicket();
- }
- }.start();
- }
- }
- public void sellTicket() {
- ticket--;
- System.out.println("剩餘的票数:" + ticket);
- }
- }
複製代碼
這裡我們通過開啟十個線程來購買火車票,不過火車票只有十張,下面通過打印信息來看一下搶票的情況 - 剩余的票数:9
- 剩余的票数:8
- 剩余的票数:7
- 剩余的票数:6
- 剩余的票数:5
- 剩余的票数:1
- 剩余的票数:1
- 剩余的票数:1
- 剩余的票数:1
- 剩余的票数:0
複製代碼
可以發現,票數出現了誤差,這明顯就是不行的,這也是因為開啟了十個線程,大家都搶著自己的票。上面這種情況是因為其中有四個線程都擠在一起了,然後一起執行了【ticket–;】,接著再一起執行【System.out.println(“剩餘的票數:” + ticket);】導致的。那麼該如何保證大家都是能夠自覺排隊,井然有序的搶票呢。這個時候就要用到synchronized關鍵字 方法一:我們在方法上添加synchronized關鍵字 - public class SyncActivity extends AppCompatActivity {
- private int ticket = 10;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_sync);
- for (int i = 0; i < 10; i++) {
- new Thread() {
- @Override
- public void run() {
- //买票
- sellTicket();
- }
- }.start();
- }
- }
- //添加在这里
- public synchronized void sellTicket() {
- ticket--;
- System.out.println("剩余的票数:" + ticket);
- }
- }
複製代碼
這樣就表示這個方法是同步的,只能由一個個線程來爭奪裡面的資源,下面通過打印信息可以驗證 - 剩余的票数:9
- 剩余的票数:8
- 剩余的票数:7
- 剩余的票数:6
- 剩余的票数:5
- 剩余的票数:4
- 剩余的票数:3
- 剩余的票数:2
- 剩余的票数:1
- 剩余的票数:0
複製代碼
方法二:我們在方法內添加synchronized關鍵字 - public class SyncActivity extends AppCompatActivity {
- private int ticket = 10;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_sync);
- for (int i = 0; i < 10; i++) {
- new Thread() {
- @Override
- public void run() {
- //买票
- sellTicket();
- }
- }.start();
- }
- }
- //添加在这里
- Object lock = new Object();
- public void sellTicket() {
- synchronized(lock){
- ticket--;
- System.out.println("剩余的票数:" + ticket);
- }
- }
- }
複製代碼
其實,synchronized關鍵字可以理解為一個鎖,而鎖就需要被鎖的東西,所以synchronized又分為類鎖和對象鎖,即可以鎖類又可以鎖對象,它們共同的作用就是保證線程的同步。就好比如我們上面中synchronized(lock),就是對象鎖,將Object對象鎖起來
一、類鎖和對象鎖的概念
對象鎖和類鎖在鎖的概念上基本上和內置鎖是一致的,但是在多線程訪問時,兩個鎖實際是有很大的區別的,對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對像上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以,結論是:1、不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。2、而且類鎖和對象鎖互相不干擾。 二、對象鎖
類鎖創建如下兩種方法 - public class SynchronizedDemo {
- //同步方法,对象锁
- public synchronized void syncMethod() {
- }
- //同步块,对象锁
- public void syncThis() {
- synchronized (this) {
- }
- }
- }
複製代碼
三、類鎖
對象鎖創建如下兩種方法 - public class SynchronizedDemo {
- //同步class对象,类锁
- public void syncClassMethod() {
- synchronized (SynchronizedDemo.class) {
- }
- }
- //同步静态方法,类锁
- public static synchronized void syncStaticMethod(){
- }
- }
複製代碼
四、通過例子理解結論和概念
根據類鎖和對象鎖的概念,我們來通過例子驗證一下其正確性,這裡演示兩個對象鎖和一個類鎖,我們創建一個類 - public class SynchronizedDemo {
- private int ticket = 10;
- //同步方法,对象锁
- public synchronized void syncMethod() {
- for (int i = 0; i < 1000; i++) {
- ticket--;
- System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
- }
- }
- //同步块,对象锁
- public void syncThis() {
- synchronized (this) {
- for (int i = 0; i < 1000; i++) {
- ticket--;
- System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
- }
- }
- }
- //同步class对象,类锁
- public void syncClassMethod() {
- synchronized (SynchronizedDemo.class) {
- for (int i = 0; i < 50; i++) {
- ticket--;
- System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
- }
- }
- }
- }
複製代碼
情況一:同一個對象,使用兩個線程調用不同對象鎖 - protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_sync);
- final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
- //线程一
- new Thread() {
- @Override
- public void run() {
- synchronizedDemo.syncMethod();
- }
- }.start();
- //线程二
- new Thread() {
- @Override
- public void run() {
- synchronizedDemo.syncThis();
- }
- }.start();
- }
複製代碼
由於使用的是同一個對象的對象鎖,所以執行出來的結果是同步的(即先運行線程一,等線程一運行完後運行線程二,ticket有序的減少),這裡使用1000比較大的數字是為了一次能看出效果 - Thread-1611剩余的票数:7
- Thread-1611剩余的票数:6
- Thread-1611剩余的票数:5
- Thread-1611剩余的票数:4
- Thread-1611剩余的票数:3
- Thread-1611剩余的票数:2
複製代碼
情況二:不同對象,使用兩個線程調用同個對象鎖 - protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_sync);
- final SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
- final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
- //线程一
- new Thread() {
- @Override
- public void run() {
- synchronizedDemo1.syncMethod();
- }
- }.start();
- //线程二
- new Thread() {
- @Override
- public void run() {
- synchronizedDemo2.syncMethod();
- }
- }.start();
- }
複製代碼
由於是不同對象,所以執行的對象鎖都不是不同的,其結果是兩個線程互相搶占資源的運行,即ticket偶爾會無序的減少 - Thread-1667剩余的票数:-1612
- Thread-1667剩余的票数:-1613
- Thread-1668剩余的票数:-1630
- Thread-1668剩余的票数:-1631
- Thread-1668剩余的票数:-1632
複製代碼
情況三:同一個對象,使用兩個線程調用一個對象鎖一個類鎖 - protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_sync);
- final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
- //线程一
- new Thread() {
- @Override
- public void run() {
- synchronizedDemo.syncMethod();
- }
- }.start();
- //线程二
- new Thread() {
- @Override
- public void run() {
- synchronizedDemo.syncClassMethod();
- }
- }.start();
- }
複製代碼
由於對象鎖和類鎖互不干擾,所以也是線程不安全的 - Thread-1667剩余的票数:-1612
- Thread-1667剩余的票数:-1613
- Thread-1668剩余的票数:-1630
- Thread-1668剩余的票数:-1631
- Thread-1668剩余的票数:-1632
複製代碼
這裡再溫習一下結論:1、不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。2、而且類鎖和對象鎖互相不干擾。 不知不覺synchronized介紹了那麼多,本可以放單獨一篇文章的,不過後面的不多,認真看的人應該有點收穫 線程同步之ReentrantLock鎖Java6.0增加了一種新的機制:ReentrantLock。ReentrantLock比synchronized理解簡單多了,下面看ReentrantLock的使用 - public class RenntrantLockActivity extends AppCompatActivity {
- Lock lock;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_renntrant_lock);
- lock = new ReentrantLock();
- doSth();
- }
- public void doSth() {
- lock.lock();
- try {
- //这里执行线程同步操作
- } finally {
- lock.unlock();
- }
- }
- }
複製代碼
使用ReentrantLock很好理解,就好比我們現實的鎖頭是一樣道理的。使用ReentrantLock的一般組合是lock與unlock成對出現的,需要注意的是,千萬不要忘記調用unlock來釋放鎖,否則可能會引發死鎖等問題。如果忘記了在finally塊中釋放鎖,可能會在程序中留下一個定時炸彈,隨時都會炸了,而是用synchronized,JVM將確保鎖會獲得自動釋放,這也是為什麼Lock沒有完全替代掉synchronized的原因 線程的生命週期的介紹線程也有屬於自己的生命週期,這裡使用我畫的一張圖來理解,在下面我們會講解這個有關生命週期的一些方法的使用 線程的等待喚醒機制之wait()、notify()、notifyAll()一開始我們也提到了wait、notify、notifyAll都必須在synchronized中執行,否則會拋出異常。所以下面以一個簡單的例子來介紹線程的等待喚醒機制 - public class WaitAndNotifyActivity extends AppCompatActivity {
- private static Object lockObject = new Object();
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_wait_and_notify);
- System.out.println("主线程运行");
- //创建子线程
- Thread thread = new WaitThread();
- thread.start();
- long start = System.currentTimeMillis();
- synchronized (lockObject) {
- try {
- System.out.println("主线程等待");
- lockObject.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("主线程继续 --> 等待的时间:" + (System.currentTimeMillis() - start));
- }
- }
- class WaitThread extends Thread {
- @Override
- public void run() {
- synchronized (lockObject) {
- try {
- //子线程等待了2秒钟后唤醒lockObject锁
- Thread.sleep(2000);
- lockObject.notifyAll();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
複製代碼
可以看到,我們使用的是同一個對象的鎖,和同一個對象執行的wait()和notify()才會保證了我們的線程同步。當主線程執行到wait()方法時,代表主線程等待,讓出使用權讓子線程執行,這個時候主線程等待這一事件會被加進到【等待喚醒的隊列】中。然後子線程則是兩秒鐘後執行notify()方法喚醒等待【喚醒隊列中】的第一個線程,這裡指的是主線程。而notifyAll()方法則是喚醒整個【喚醒隊列中】的所有線程,這裡就不多加演示了 下面採用一道經典的Java多線程面試題來讓大家練習熟悉熟悉:子線程循環10次,接著主線程循環15次,接著又回到子線程循環10次,接著再回到主線程又循環15次,如此循環50次 - //子线程
- new Thread() {
- @Override
- public void run() {
- for (int i = 0; i < 50; i++) {
- for (int j = 0; j < 10; j++) {
- System.out.println("子循环循环第" + (j + 1) + "次");
- }
- System.out.println("--> 子线程循环了" + (i + 1) + "次");
- }
- }
- }.start();
- //主线程
- for (int i = 0; i < 50; i++) {
- for (int j = 0; j < 15; j++) {
- System.out.println("主循环循环第" + (j + 1) + "次");
- }
- System.out.println("--> 主线程循环了" + (i + 1) + "次");
- }
複製代碼
首先是主要思路的搭建,現在的問題就是如何讓子線程和主線程有序的執行呢,那肯定是我們的等待喚醒機制 - //子线程
- new Thread() {
- @Override
- public void run() {
- for (int i = 0; i < 50; i++) {
- synchronized (lock){
- for (int j = 0; j < 10; j++) {
- System.out.println("子循环循环第" + (j + 1) + "次");
- }
- //唤醒
- lock.notify();
- //等待
- try {
- lock.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }.start();
- //主线程
- for (int i = 0; i < 50; i++) {
- synchronized (lock){
- //等待
- try {
- lock.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- for (int j = 0; j < 15; j++) {
- System.out.println("主循环循环第" + (j + 1) + "次");
- }
- //唤醒
- lock.notify();
- }
- }
複製代碼
不管是主線程先運行還是子線程運行,兩個線程只能同時進入synchronized (lock)一個鎖中。由於是子線程先運行:1、當主線程先進入synchronized (lock)鎖時,它就必須是等待,而子線程開始運行輸出,輸出後就喚醒主線程。2、當子線程先運行的話,那就直接輸出,然後等待主線程的運行輸出 線程的sleep()、join()、yield()一、sleep()
sleep()作用是讓線程休息指定的時間,時間一到就繼續運行,它的使用很簡單 - try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
複製代碼
二、join()
join()作用是讓指定的線程先執行完再執行其他線程,而且會阻塞主線程,它的使用也很簡單 - public class JoinActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_join);
- //启动线程一
- try {
- MyThread myThread1 = new MyThread("线程一");
- myThread1.start();
- myThread1.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("主线程需要等待");
- //启动线程二
- try {
- MyThread myThread2 = new MyThread("线程二");
- myThread2.start();
- myThread2.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("主线程继续执行");
- }
- class MyThread extends Thread {
- public MyThread(String name) {
- super(name);
- }
- @Override
- public void run() {
- System.out.println(getName() + "在运行");
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
複製代碼
這裡就不解釋了,看打印信息,你就能發現它的作用了 - 线程一在运行
- 主线程需要等待
- 线程二在运行
- 主线程继续执行
複製代碼
三、yield()
yield()的作用是指定線程先禮讓一下別的線程的先執行,就好比公交車只有一個座位,誰禮讓了誰就坐上去。特別注意的是:yield()會禮讓給相同優先級的或者是優先級更高的線程執行,不過yield()這個方法只是把線程的執行狀態打回準備就緒狀態,所以執行了該方法後,有可能馬上又開始運行,有可能等待很長時間 - public class YieldActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_yield);
- MyThread myThread1 = new MyThread("线程一");
- MyThread myThread2 = new MyThread("线程二");
- myThread1.start();
- myThread2.start();
- }
- class MyThread extends Thread {
- public MyThread(String name) {
- super(name);
- }
- @Override
- public synchronized void run() {
- for (int i = 0; i < 100; i++) {
- System.out.println(getName() + "在运行,i的值为:" + i + " 优先级为:" + getPriority());
- if (i == 2) {
- System.out.println(getName() + "礼让");
- Thread.yield();
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
- }
複製代碼
這裡我們通過Thread.sleep()的方式,讓線程強行延遲一秒回到準備就緒狀態,這樣在打印信息上就能看到我們想要的結果了 - 线程二在运行,i的值为:0 优先级为:5
- 线程二在运行,i的值为:1 优先级为:5
- 线程二在运行,i的值为:2 优先级为:5
- 线程二礼让
- 线程一在运行,i的值为:0 优先级为:5
- 线程一在运行,i的值为:1 优先级为:5
- 线程一在运行,i的值为:2 优先级为:5
- 线程一礼让
- 线程二在运行,i的值为:3 优先级为:5
- 线程二在运行,i的值为:4 优先级为:5
- 线程二在运行,i的值为:5 优先级为:5
- 线程二在运行,i的值为:6 优先级为:5
- ......
複製代碼
好了,關於線程的介紹就這麼多,可能知識點有點多,我自己也學習了好幾天來掌握線程,這裡的分享我都是測試過的。學習一遍才知道原來是這麼一回事,沒學習之前看別人的文章還是懂的,當自己碼一遍的時候會發現寫不出來,原因是沒有真正理解線程。現在理解了線程之後,寫出來會根據它的作用和思路來寫,根本不用記代碼
|