TShopping

 找回密碼
 註冊
搜索
查看: 1613|回復: 0
打印 上一主題 下一主題

[教學] 多線程系列之wait、notify、sleep、join、yield、synchronized關鍵...

[複製鏈接]
跳轉到指定樓層
1#
發表於 2017-8-9 17:53:19 | 只看該作者 |只看大圖 回帖獎勵 |倒序瀏覽 |閱讀模式
 
Push to Facebook
前言
多線程一直是初學者最困惑的地方,每次看到一篇文章,覺得很有難度,就馬上叉掉,不看了,我以前也是這樣過來的。後來,我發現這樣的態度不行,知難而退,永遠進步不了。於是,我狠下心來看完別人的博客,儘管很難但還是咬著牙,不懂去查閱資料,到最後弄懂整個過程。雖然花費時間很大,但這就是自學的精髓,別人學不會,而我卻學到了。很簡單的一個例子,一開始我對自定義View也是很抵觸,看到很難的圖就不去思考他,故意避開它,然而當我看到自己喜歡的雷達圖時,很有興趣的去查閱資料,不知不覺,自定義View對我已經沒有難度了。所以對於多線程我也是0基礎,不過我還是咬著牙皮,該學的還是得學。這裡先總結這幾個類特點和區別,讓大家帶著模糊印象來學習這篇文章
  • Thread是個線程,而且有自己的生命週期
  • 對於線程常用的操作有:wait(等待)、notify(喚醒)、notifyAll、sleep(睡眠)、join(阻塞)、yield(禮讓)
  • wait、notify、notifyAll都必須在synchronized中執行,否則會拋出異常
  • synchronized關鍵字和ReentrantLock鎖都是輔助線程同步使用的
  • 初學者常犯的誤區:一個對像只有一個鎖(正確的)

本篇文章包含以下內容
線程同步之synchronized關鍵字
馬上就過年了,火車搶票又是一年沸沸揚揚的事情,這也就好比我們的多線程搶奪資源是一個道理,下面我們通過火車搶票的案例來理解
  1. public class SyncActivity extends AppCompatActivity {

  2.     private int ticket = 10;

  3.     @Override
  4.     protected void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.activity_sync);

  7.         for (int i = 0; i < 10; i++) {
  8.             new Thread() {
  9.                 @Override
  10.                 public void run() {
  11.                     //買票
  12.                     sellTicket();
  13.                 }
  14.             }.start();
  15.         }
  16.     }

  17.     public void sellTicket() {
  18.         ticket--;
  19.         System.out.println("剩餘的票数:" + ticket);
  20.     }
  21. }
複製代碼


這裡我們通過開啟十個線程來購買火車票,不過火車票只有十張,下面通過打印信息來看一下搶票的情況
  1. 剩余的票数:9
  2. 剩余的票数:8
  3. 剩余的票数:7
  4. 剩余的票数:6
  5. 剩余的票数:5
  6. 剩余的票数:1
  7. 剩余的票数:1
  8. 剩余的票数:1
  9. 剩余的票数:1
  10. 剩余的票数:0
複製代碼

可以發現,票數出現了誤差,這明顯就是不行的,這也是因為開啟了十個線程,大家都搶著自己的票。上面這種情況是因為其中有四個線程都擠在一起了,然後一起執行了【ticket–;】,接著再一起執行【System.out.println(“剩餘的票數:” + ticket);】導致的。那麼該如何保證大家都是能夠自覺排隊,井然有序的搶票呢。這個時候就要用到synchronized關鍵字
方法一:我們在方法上添加synchronized關鍵字
  1. public class SyncActivity extends AppCompatActivity {

  2.     private int ticket = 10;

  3.     @Override
  4.     protected void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.activity_sync);

  7.         for (int i = 0; i < 10; i++) {
  8.             new Thread() {
  9.                 @Override
  10.                 public void run() {
  11.                     //买票
  12.                     sellTicket();
  13.                 }
  14.             }.start();
  15.         }
  16.     }

  17.     //添加在这里
  18.     public synchronized void sellTicket() {
  19.         ticket--;
  20.         System.out.println("剩余的票数:" + ticket);
  21.     }
  22. }
複製代碼

這樣就表示這個方法是同步的,只能由一個個線程來爭奪裡面的資源,下面通過打印信息可以驗證
  1. 剩余的票数:9
  2. 剩余的票数:8
  3. 剩余的票数:7
  4. 剩余的票数:6
  5. 剩余的票数:5
  6. 剩余的票数:4
  7. 剩余的票数:3
  8. 剩余的票数:2
  9. 剩余的票数:1
  10. 剩余的票数:0
複製代碼

方法二:我們在方法內添加synchronized關鍵字
  1. public class SyncActivity extends AppCompatActivity {

  2.     private int ticket = 10;

  3.     @Override
  4.     protected void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.activity_sync);

  7.         for (int i = 0; i < 10; i++) {
  8.             new Thread() {
  9.                 @Override
  10.                 public void run() {
  11.                     //买票
  12.                     sellTicket();
  13.                 }
  14.             }.start();
  15.         }
  16.     }

  17.     //添加在这里
  18.     Object lock = new Object();
  19.     public void sellTicket() {
  20.         synchronized(lock){
  21.             ticket--;
  22.             System.out.println("剩余的票数:" + ticket);
  23.         }
  24.     }
  25. }
複製代碼

其實,synchronized關鍵字可以理解為一個鎖,而鎖就需要被鎖的東西,所以synchronized又分為類鎖和對象鎖,即可以鎖類又可以鎖對象,它們共同的作用就是保證線程的同步。就好比如我們上面中synchronized(lock),就是對象鎖,將Object對象鎖起來

一、類鎖和對象鎖的概念
對象鎖和類鎖在鎖的概念上基本上和內置鎖是一致的,但是在多線程訪問時,兩個鎖實際是有很大的區別的,對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對像上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以,結論是:1、不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。2、而且類鎖和對象鎖互相不干擾。
二、對象鎖
類鎖創建如下兩種方法
  1. public class SynchronizedDemo {
  2.     //同步方法,对象锁
  3.     public synchronized void syncMethod() {

  4.     }

  5.     //同步块,对象锁
  6.     public void syncThis() {
  7.         synchronized (this) {

  8.         }
  9.     }
  10. }
複製代碼

三、類鎖
對象鎖創建如下兩種方法
  1. public class SynchronizedDemo {
  2.     //同步class对象,类锁
  3.     public void syncClassMethod() {
  4.         synchronized (SynchronizedDemo.class) {

  5.         }
  6.     }

  7.     //同步静态方法,类锁
  8.     public static synchronized void syncStaticMethod(){

  9.     }
  10. }
複製代碼

四、通過例子理解結論和概念
根據類鎖和對象鎖的概念,我們來通過例子驗證一下其正確性,這裡演示兩個對象鎖和一個類鎖,我們創建一個類
  1. public class SynchronizedDemo {
  2.      private int ticket = 10;
  3.     //同步方法,对象锁
  4.     public synchronized void syncMethod() {
  5.         for (int i = 0; i < 1000; i++) {
  6.             ticket--;
  7.             System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
  8.         }
  9.     }

  10.     //同步块,对象锁
  11.     public void syncThis() {
  12.         synchronized (this) {
  13.             for (int i = 0; i < 1000; i++) {
  14.                 ticket--;
  15.                 System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
  16.             }
  17.         }
  18.     }

  19.     //同步class对象,类锁
  20.     public void syncClassMethod() {
  21.         synchronized (SynchronizedDemo.class) {
  22.             for (int i = 0; i < 50; i++) {
  23.                 ticket--;
  24.                 System.out.println(Thread.currentThread().getName() + "剩余的票数:" + ticket);
  25.             }
  26.         }
  27.     }
  28. }
複製代碼

情況一:同一個對象,使用兩個線程調用不同對象鎖
  1. protected void onCreate(Bundle savedInstanceState) {
  2.     super.onCreate(savedInstanceState);
  3.     setContentView(R.layout.activity_sync);

  4.     final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

  5.     //线程一
  6.     new Thread() {
  7.         @Override
  8.         public void run() {
  9.             synchronizedDemo.syncMethod();
  10.         }
  11.     }.start();
  12.     //线程二
  13.     new Thread() {
  14.         @Override
  15.         public void run() {
  16.             synchronizedDemo.syncThis();
  17.         }
  18.     }.start();
  19. }
複製代碼

由於使用的是同一個對象的對象鎖,所以執行出來的結果是同步的(即先運行線程一,等線程一運行完後運行線程二,ticket有序的減少),這裡使用1000比較大的數字是為了一次能看出效果
  1. Thread-1611剩余的票数:7
  2. Thread-1611剩余的票数:6
  3. Thread-1611剩余的票数:5
  4. Thread-1611剩余的票数:4
  5. Thread-1611剩余的票数:3
  6. Thread-1611剩余的票数:2
複製代碼

情況二:不同對象,使用兩個線程調用同個對象鎖
  1. protected void onCreate(Bundle savedInstanceState) {
  2.     super.onCreate(savedInstanceState);
  3.     setContentView(R.layout.activity_sync);

  4.     final SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
  5.     final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();

  6.     //线程一
  7.     new Thread() {
  8.         @Override
  9.         public void run() {
  10.             synchronizedDemo1.syncMethod();
  11.         }
  12.     }.start();
  13.     //线程二
  14.     new Thread() {
  15.         @Override
  16.         public void run() {
  17.             synchronizedDemo2.syncMethod();
  18.         }
  19.     }.start();
  20. }
複製代碼

由於是不同對象,所以執行的對象鎖都不是不同的,其結果是兩個線程互相搶占資源的運行,即ticket偶爾會無序的減少
  1. Thread-1667剩余的票数:-1612
  2. Thread-1667剩余的票数:-1613
  3. Thread-1668剩余的票数:-1630
  4. Thread-1668剩余的票数:-1631
  5. Thread-1668剩余的票数:-1632
複製代碼

情況三:同一個對象,使用兩個線程調用一個對象鎖一個類鎖
  1. protected void onCreate(Bundle savedInstanceState) {
  2.     super.onCreate(savedInstanceState);
  3.     setContentView(R.layout.activity_sync);

  4.     final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

  5.     //线程一
  6.     new Thread() {
  7.         @Override
  8.         public void run() {
  9.             synchronizedDemo.syncMethod();
  10.         }
  11.     }.start();
  12.     //线程二
  13.     new Thread() {
  14.         @Override
  15.         public void run() {
  16.             synchronizedDemo.syncClassMethod();
  17.         }
  18.     }.start();
  19. }
複製代碼

由於對象鎖和類鎖互不干擾,所以也是線程不安全的
  1. Thread-1667剩余的票数:-1612
  2. Thread-1667剩余的票数:-1613
  3. Thread-1668剩余的票数:-1630
  4. Thread-1668剩余的票数:-1631
  5. Thread-1668剩余的票数:-1632
複製代碼

這裡再溫習一下結論:1、不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。2、而且類鎖和對象鎖互相不干擾。
不知不覺synchronized介紹了那麼多,本可以放單獨一篇文章的,不過後面的不多,認真看的人應該有點收穫
線程同步之ReentrantLock鎖
Java6.0增加了一種新的機制:ReentrantLock。ReentrantLock比synchronized理解簡單多了,下面看ReentrantLock的使用
  1. public class RenntrantLockActivity extends AppCompatActivity {

  2.     Lock lock;

  3.     @Override
  4.     protected void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.activity_renntrant_lock);

  7.         lock = new ReentrantLock();
  8.         doSth();
  9.     }

  10.     public void doSth() {
  11.         lock.lock();
  12.         try {
  13.             //这里执行线程同步操作

  14.         } finally {
  15.             lock.unlock();
  16.         }
  17.     }
  18. }
複製代碼

使用ReentrantLock很好理解,就好比我們現實的鎖頭是一樣道理的。使用ReentrantLock的一般組合是lock與unlock成對出現的,需要注意的是,千萬不要忘記調用unlock來釋放鎖,否則可能會引發死鎖等問題。如果忘記了在finally塊中釋放鎖,可能會在程序中留下一個定時炸彈,隨時都會炸了,而是用synchronized,JVM將確保鎖會獲得自動釋放,這也是為什麼Lock沒有完全替代掉synchronized的原因
線程的生命週期的介紹
線程也有屬於自己的生命週期,這裡使用我畫的一張圖來理解,在下面我們會講解這個有關生命週期的一些方法的使用
線程的等待喚醒機制之wait()、notify()、notifyAll()
一開始我們也提到了wait、notify、notifyAll都必須在synchronized中執行,否則會拋出異常。所以下面以一個簡單的例子來介紹線程的等待喚醒機制
  1. public class WaitAndNotifyActivity extends AppCompatActivity {

  2.     private static Object lockObject = new Object();

  3.     @Override
  4.     protected void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.activity_wait_and_notify);

  7.         System.out.println("主线程运行");
  8.         //创建子线程
  9.         Thread thread = new WaitThread();
  10.         thread.start();

  11.         long start = System.currentTimeMillis();
  12.         synchronized (lockObject) {
  13.             try {
  14.                 System.out.println("主线程等待");
  15.                 lockObject.wait();
  16.             } catch (InterruptedException e) {
  17.                 e.printStackTrace();
  18.             }
  19.             System.out.println("主线程继续 --> 等待的时间:" + (System.currentTimeMillis() - start));
  20.         }
  21.     }

  22.     class WaitThread extends Thread {
  23.         @Override
  24.         public void run() {
  25.             synchronized (lockObject) {
  26.                 try {
  27.                     //子线程等待了2秒钟后唤醒lockObject锁
  28.                     Thread.sleep(2000);
  29.                     lockObject.notifyAll();
  30.                 } catch (InterruptedException e) {
  31.                     e.printStackTrace();
  32.                 }
  33.             }
  34.         }
  35.     }
  36. }
複製代碼

可以看到,我們使用的是同一個對象的鎖,和同一個對象執行的wait()和notify()才會保證了我們的線程同步。當主線程執行到wait()方法時,代表主線程等待,讓出使用權讓子線程執行,這個時候主線程等待這一事件會被加進到【等待喚醒的隊列】中。然後子線程則是兩秒鐘後執行notify()方法喚醒等待【喚醒隊列中】的第一個線程,這裡指的是主線程。而notifyAll()方法則是喚醒整個【喚醒隊列中】的所有線程,這裡就不多加演示了
下面採用一道經典的Java多線程面試題來讓大家練習熟悉熟悉:子線程循環10次,接著主線程循環15次,接著又回到子線程循環10次,接著再回到主線程又循環15次,如此循環50次
  1. //子线程
  2. new Thread() {
  3.     @Override
  4.     public void run() {
  5.         for (int i = 0; i < 50; i++) {
  6.             for (int j = 0; j < 10; j++) {
  7.                 System.out.println("子循环循环第" + (j + 1) + "次");
  8.             }
  9.             System.out.println("--> 子线程循环了" + (i + 1) + "次");
  10.         }
  11.     }
  12. }.start();
  13. //主线程
  14. for (int i = 0; i < 50; i++) {
  15.     for (int j = 0; j < 15; j++) {
  16.         System.out.println("主循环循环第" + (j + 1) + "次");
  17.     }
  18.     System.out.println("--> 主线程循环了" + (i + 1) + "次");
  19. }
複製代碼

首先是主要思路的搭建,現在的問題就是如何讓子線程和主線程有序的執行呢,那肯定是我們的等待喚醒機制
  1. //子线程
  2. new Thread() {
  3.     @Override
  4.     public void run() {
  5.         for (int i = 0; i < 50; i++) {
  6.             synchronized (lock){

  7.                 for (int j = 0; j < 10; j++) {
  8.                     System.out.println("子循环循环第" + (j + 1) + "次");
  9.                 }
  10.                 //唤醒
  11.                 lock.notify();
  12.                 //等待
  13.                 try {
  14.                     lock.wait();
  15.                 } catch (InterruptedException e) {
  16.                     e.printStackTrace();
  17.                 }
  18.             }
  19.         }
  20.     }
  21. }.start();
  22. //主线程
  23. for (int i = 0; i < 50; i++) {
  24.     synchronized (lock){
  25.         //等待
  26.         try {
  27.             lock.wait();
  28.         } catch (InterruptedException e) {
  29.             e.printStackTrace();
  30.         }

  31.         for (int j = 0; j < 15; j++) {
  32.             System.out.println("主循环循环第" + (j + 1) + "次");
  33.         }
  34.         //唤醒
  35.         lock.notify();
  36.     }
  37. }
複製代碼

不管是主線程先運行還是子線程運行,兩個線程只能同時進入synchronized (lock)一個鎖中。由於是子線程先運行:1、當主線程先進入synchronized (lock)鎖時,它就必須是等待,而子線程開始運行輸出,輸出後就喚醒主線程。2、當子線程先運行的話,那就直接輸出,然後等待主線程的運行輸出
線程的sleep()、join()、yield()
一、sleep()
sleep()作用是讓線程休息指定的時間,時間一到就繼續運行,它的使用很簡單
  1. try {
  2.     Thread.sleep(2000);
  3. } catch (InterruptedException e) {
  4.     e.printStackTrace();
  5. }
複製代碼

二、join()
join()作用是讓指定的線程先執行完再執行其他線程,而且會阻塞主線程,它的使用也很簡單
  1. public class JoinActivity extends AppCompatActivity {

  2.     @Override
  3.     protected void onCreate(Bundle savedInstanceState) {
  4.         super.onCreate(savedInstanceState);
  5.         setContentView(R.layout.activity_join);

  6.         //启动线程一
  7.         try {
  8.             MyThread myThread1 = new MyThread("线程一");
  9.             myThread1.start();
  10.             myThread1.join();
  11.         } catch (InterruptedException e) {
  12.             e.printStackTrace();
  13.         }

  14.         System.out.println("主线程需要等待");

  15.         //启动线程二
  16.         try {
  17.             MyThread myThread2 = new MyThread("线程二");
  18.             myThread2.start();
  19.             myThread2.join();
  20.         } catch (InterruptedException e) {
  21.             e.printStackTrace();
  22.         }

  23.         System.out.println("主线程继续执行");
  24.     }

  25.     class MyThread extends Thread {

  26.         public MyThread(String name) {
  27.             super(name);
  28.         }

  29.         @Override
  30.         public void run() {
  31.             System.out.println(getName() + "在运行");
  32.             try {
  33.                 Thread.sleep(2000);
  34.             } catch (InterruptedException e) {
  35.                 e.printStackTrace();
  36.             }
  37.         }
  38.     }
  39. }
複製代碼

這裡就不解釋了,看打印信息,你就能發現它的作用了
  1. 线程一在运行
  2. 主线程需要等待
  3. 线程二在运行
  4. 主线程继续执行
複製代碼

三、yield()
yield()的作用是指定線程先禮讓一下別的線程的先執行,就好比公交車只有一個座位,誰禮讓了誰就坐上去。特別注意的是:yield()會禮讓給相同優先級的或者是優先級更高的線程執行,不過yield()這個方法只是把線程的執行狀態打回準備就緒狀態,所以執行了該方法後,有可能馬上又開始運行,有可能等待很長時間
  1. public class YieldActivity extends AppCompatActivity {

  2.     @Override
  3.     protected void onCreate(Bundle savedInstanceState) {
  4.         super.onCreate(savedInstanceState);
  5.         setContentView(R.layout.activity_yield);

  6.         MyThread myThread1 = new MyThread("线程一");
  7.         MyThread myThread2 = new MyThread("线程二");

  8.         myThread1.start();
  9.         myThread2.start();
  10.     }

  11.     class MyThread extends Thread {

  12.         public MyThread(String name) {
  13.             super(name);
  14.         }

  15.         @Override
  16.         public synchronized void run() {
  17.             for (int i = 0; i < 100; i++) {
  18.                 System.out.println(getName() + "在运行,i的值为:" + i + " 优先级为:" + getPriority());
  19.                 if (i == 2) {
  20.                     System.out.println(getName() + "礼让");
  21.                     Thread.yield();
  22.                     try {
  23.                         Thread.sleep(1000);
  24.                     } catch (InterruptedException e) {
  25.                         e.printStackTrace();
  26.                     }
  27.                 }
  28.             }
  29.         }
  30.     }
  31. }
複製代碼

這裡我們通過Thread.sleep()的方式,讓線程強行延遲一秒回到準備就緒狀態,這樣在打印信息上就能看到我們想要的結果了
  1. 线程二在运行,i的值为:0 优先级为:5
  2. 线程二在运行,i的值为:1 优先级为:5
  3. 线程二在运行,i的值为:2 优先级为:5
  4. 线程二礼让
  5. 线程一在运行,i的值为:0 优先级为:5
  6. 线程一在运行,i的值为:1 优先级为:5
  7. 线程一在运行,i的值为:2 优先级为:5
  8. 线程一礼让
  9. 线程二在运行,i的值为:3 优先级为:5
  10. 线程二在运行,i的值为:4 优先级为:5
  11. 线程二在运行,i的值为:5 优先级为:5
  12. 线程二在运行,i的值为:6 优先级为:5
  13. ......
複製代碼

好了,關於線程的介紹就這麼多,可能知識點有點多,我自己也學習了好幾天來掌握線程,這裡的分享我都是測試過的。學習一遍才知道原來是這麼一回事,沒學習之前看別人的文章還是懂的,當自己碼一遍的時候會發現寫不出來,原因是沒有真正理解線程。現在理解了線程之後,寫出來會根據它的作用和思路來寫,根本不用記代碼



 

臉書網友討論
*滑块验证:
您需要登錄後才可以回帖 登錄 | 註冊 |

本版積分規則



Archiver|手機版|小黑屋|免責聲明|TShopping

GMT+8, 2024-5-3 21:21 , Processed in 0.117159 second(s), 25 queries .

本論壇言論純屬發表者個人意見,與 TShopping綜合論壇 立場無關 如有意見侵犯了您的權益 請寫信聯絡我們。

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回復 返回頂部 返回列表