<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文看懂wait和notify的虛假喚醒(spurious wakeups)

          共 17770字,需瀏覽 36分鐘

           ·

          2021-08-13 19:14

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!

          編輯:業(yè)余草

          推薦:https://www.xttblog.com/?p=5257

          java 多線程 wait 時為什么要用 while 而不是 if?

          對于 java 多線程的wait()方法,我們在 jdk1.6 的說明文檔里可以看到這樣一段話:

          wait方法的JDK文檔說明

          從上面的截圖,我們可以看出,在使用 wait 方法時,需要使用 while 循環(huán)來判斷條件十分滿足,而不是 if,那么我們思考以下,如果使用 if 會怎么樣?

          為方便講解,我們來看一個被廣泛使用的生產(chǎn)消費的例子。demo 代碼如下:

          /*
              生產(chǎn)和消費
          */

          package multiThread;
           
          class SynStack {
              private char[] data = new char[6];
              private int cnt = 0//表示數(shù)組有效元素的個數(shù)
               
              public synchronized void push(char ch){
                  if (cnt >= data.length){
                      try{
                          System.out.println("生產(chǎn)線程"+Thread.currentThread().getName()+"準(zhǔn)備休眠");
                          this.wait();
                          System.out.println("生產(chǎn)線程"+Thread.currentThread().getName()+"休眠結(jié)束了");
                      }catch (Exception e){
                          e.printStackTrace();
                      }
                  }
                  this.notify();
                  data[cnt] = ch;
                  ++cnt;
                  System.out.printf("生產(chǎn)線程"+Thread.currentThread().getName()+"正在生產(chǎn)第%d個產(chǎn)品,該產(chǎn)品是: %c\n", cnt, ch);
              }
               
              public synchronized char pop(){
                  char ch;
                  if (cnt <= 0) {
                      try{
                          System.out.println("消費線程"+Thread.currentThread().getName()+"準(zhǔn)備休眠");
                          this.wait();
                          System.out.println("消費線程"+Thread.currentThread().getName()+"休眠結(jié)束了");
                      }catch (Exception e){
                          e.printStackTrace();
                      }
                  }
                  this.notify();
                  ch = data[cnt-1];
                  System.out.printf("消費線程"+Thread.currentThread().getName()+"正在消費第%d個產(chǎn)品,該產(chǎn)品是: %c\n", cnt, ch);
                  --cnt;
                  return ch;     
              }  
          }
           
          class Producer implements Runnable{
              private SynStack ss = null;
              public Producer(SynStack ss){
                  this.ss = ss;
              }
               
              public void run() {
                  char ch;
                  for (int i=0; i<10; ++i){
          //          try{
          //              Thread.sleep(100);
          //          }catch (Exception e){}
                           
                      ch = (char)('a'+i);
                      ss.push(ch);
                  }
              }
          }
           
          class Consumer implements Runnable{
              private SynStack ss = null;
               
              public Consumer(SynStack ss){
                  this.ss = ss;
              }
               
              public void run(){
                  for (int i=0; i<10; ++i){
                      /*try{
                      Thread.sleep(100);
                      }
                      catch (Exception e){           
                      }*/

                       
                      //System.out.printf("%c\n", ss.pop());
                      ss.pop();
                  }
              }
          }
           
           
          public class TestPC2 {
              public static void main(String[] args) {
                  SynStack ss = new SynStack();
                  Producer p = new Producer(ss);
                  Consumer c = new Consumer(ss);
                   
                   
                  Thread t1 = new Thread(p);
                  t1.setName("1號");
                  t1.start();
                  /*Thread t2 = new Thread(p);
                  t2.setName("2號");
                  t2.start();*/

                           
                  Thread t6 = new Thread(c);
                  t6.setName("6號");
                  t6.start();
                  /*Thread t7 = new Thread(c);
                  t7.setName("7號");
                  t7.start();*/

              }
          }

          上面的代碼只有一個消費者線程和一個生產(chǎn)者線程,程序運行完美,沒有任何錯誤,那為為什么 jdk 里面強調(diào)要用 while 呢?

          這個問題,在我剛?cè)胄械臅r候,我也看不懂。那時也想了很久,然后遇到了一個好的 CTO 點撥了我一下。這個程序如果用到多個生產(chǎn)者和消費者的情況,就會出錯。然后,我將信將疑的試了一下,確實會出錯。但是我不能明白為什么就會出錯,繼續(xù)問他,他看我好學(xué)的勁頭,滿意的笑了笑:“看好你的未來!”。

          昨天,微信群里有一個網(wǎng)友在面試時,被問到了 wait 方法為什么必須寫在 while 循環(huán)中?他沒回答出來。

          而且,這個問題看了 demo 代碼后,還提問到,不是有 synchronized 關(guān)鍵字加鎖了嗎?哪里還有問題?

          如果你也有這樣的疑問,那說明你對 wait 方法原理的實際運行效果不是很了解,或者也存在錯誤的理解。我在群里對他們說,在 wait 方法的前后都加上輸出提示語句,后來的打印結(jié)果出乎他們意料。

          一個線程執(zhí)行了 wait 方法以后,它不會再繼續(xù)執(zhí)行了,直到被 notify 喚醒。

          那么喚醒以后從何處開始執(zhí)行?

          這是解決這里出錯原因的關(guān)鍵。

          我們嘗試修改代碼,實現(xiàn)一個生產(chǎn)線程,兩個消費線程。

          /*
              生產(chǎn)和消費
          */

          package multiThread;

          class SynStack {
              private char[] data = new char[6];
              private int cnt = 0//表示數(shù)組有效元素的個數(shù)
              
              public synchronized void push(char ch) {
                  if (cnt >= data.length) {
                      try {
                          System.out.println("生產(chǎn)線程"+Thread.currentThread().getName()+"準(zhǔn)備休眠");
                          this.wait();
                          System.out.println("生產(chǎn)線程"+Thread.currentThread().getName()+"休眠結(jié)束了");
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                  }
                  this.notify(); 
                  data[cnt] = ch;
                  ++cnt;
                  System.out.printf("生產(chǎn)線程"+Thread.currentThread().getName()+"正在生產(chǎn)第%d個產(chǎn)品,該產(chǎn)品是: %c\n", cnt, ch);
              }
              
              public synchronized char pop()  {
                  char ch;
                  if (cnt <= 0) {
                      try {
                          System.out.println("消費線程"+Thread.currentThread().getName()+"準(zhǔn)備休眠");
                          this.wait();
                          System.out.println("消費線程"+Thread.currentThread().getName()+"休眠結(jié)束了");
                      }  catch (Exception e) {
                          e.printStackTrace();
                      }
                  }
                  this.notify();
                  ch = data[cnt-1];
                  System.out.printf("消費線程"+Thread.currentThread().getName()+"正在消費第%d個產(chǎn)品,該產(chǎn)品是: %c\n", cnt, ch);
                  --cnt;
                  return ch;        
              }    
          }

          class Producer implements Runnable {
              private SynStack ss = null;
              public Producer(SynStack ss) {
                  this.ss = ss;
              }
              
              public void run() {
                  char ch;
                  for (int i=0; i<10; ++i) {
          //            try{
          //            Thread.sleep(100);
          //            }
          //            catch (Exception e){            
          //            }
                          
                      ch = (char)('a'+i);
                      ss.push(ch);
                  }
              }
          }

          class Consumer implements Runnable {
              private SynStack ss = null;
              
              public Consumer(SynStack ss) {
                  this.ss = ss;
              }
              
              public void run() {
                  for (int i=0; i<10; ++i) {
                      /*try{
                      Thread.sleep(100);
                      }
                      catch (Exception e){            
                      }*/

                      
                      //System.out.printf("%c\n", ss.pop());
                      ss.pop();
                  }
              }
          }


          public class TestPC2 {
              public static void main(String[] args)  {
                  SynStack ss = new SynStack();
                  Producer p = new Producer(ss);
                  Consumer c = new Consumer(ss);
                  
                  
                  Thread t1 = new Thread(p);
                  t1.setName("1號");
                  t1.start();
                  /*Thread t2 = new Thread(p);
                  t2.setName("2號");
                  t2.start();*/

                          
                  Thread t6 = new Thread(c);
                  t6.setName("6號");
                  t6.start();
                  Thread t7 = new Thread(c);
                  t7.setName("7號");
                  t7.start();
              }
          }

          上面代碼就是在 main 函數(shù)里增加了一個消費線程。

          然后錯誤出現(xiàn)了。

          虛假喚醒導(dǎo)致的程序錯誤

          數(shù)組越界,為什么會這樣?

          問題的關(guān)鍵就在于7號消費線程喚醒了 6 號消費線程,而 6 號消費線程被喚醒以后,它從哪里開始執(zhí)行是關(guān)鍵?。。。?/p>

          它會執(zhí)行

          System.out.println("消費線程"+Thread.currentThread().getName()+"休眠結(jié)束了");

          這行代碼。

          不是從 pop() 方法的開始處執(zhí)行。

          那么這跟使用 if 方法有什么關(guān)系?

          因為,7 號線程喚醒了 6 號線程,并執(zhí)行了以下 4 行代碼。

          ch = data[cnt-1];
          System.out.printf("消費線程"+Thread.currentThread().getName()+"正在消費第%d個產(chǎn)品,該產(chǎn)品是: %c\n", cnt, ch);
          --cnt;
          return ch; 

          7 號線程執(zhí)行完上面的代碼后,cnt 就 =0 了

          又因為 6 號線程被喚醒時已經(jīng)處在 if 方法體內(nèi),它不會再去執(zhí)行 if 條件判斷,所以就順序往下執(zhí)行,這個時候執(zhí)行

          ch = data[cnt-1];
          // 就會出現(xiàn)越界異常。
          // 假如使用 while 就不會,因為當(dāng)喚醒了 6 號線程以后,它依然會去執(zhí)行循環(huán)條件檢測。
          // 所以不可能執(zhí)行下去,保證了程序的安全。

          結(jié)論:就是用 if 判斷的話,喚醒后線程會從 wait 之后的代碼開始運行,但是不會重新判斷 if 條件,直接繼續(xù)運行 if 代碼塊之后的代碼,而如果使用 while 的話,也會從 wait 之后的代碼運行,但是喚醒后會重新判斷循環(huán)條件,如果不成立再執(zhí)行 while 代碼塊之后的代碼塊,成立的話繼續(xù) wait。

          這種現(xiàn)象,也就是 JDK 文檔中提到的虛假喚醒,也有人稱為:異常喚醒,虛擬喚醒、偽喚醒。

          虛假喚醒(spurious wakeup),是不想喚醒它或者說不確定是否應(yīng)該喚醒,但是被喚醒了。對程序來說,wait 方法應(yīng)該卡住當(dāng)前程序,不應(yīng)該往后執(zhí)行;但是實際上并沒有被卡住,而是在非預(yù)期的時間程序正常執(zhí)行了,沒有程序沒有被卡住就是被虛假喚醒了。

          用 while 而不是 if 來判斷,可以避免虛假喚醒。是因為操作系統(tǒng)的通知不可信,自己再校驗一次,如果是虛假喚醒就再 wait 一次(直到正確為止)。

          虛假喚醒是很多語言都存在的問題,也是很多操作系統(tǒng)底層的問題,與具體應(yīng)用無關(guān)。

          我列舉的生產(chǎn)者消費者例子,我在用通俗的白話解釋一下。

          1. 在單消費者和單生產(chǎn)者的模式中,因為只有兩個線程,消費者 pop 方法 notify 通知到的一定是生產(chǎn)者線程,使其執(zhí)行 push 操作。
          2. 在多消費者模式中,消費者 pop 方法 notify 隨機通知一個 SysStack 對象等待池中的線程,使其進(jìn)入 SysStack 對象的鎖池中競爭獲取該對象的鎖。產(chǎn)生錯誤的關(guān)鍵原因在于 notify 通知到的線程既可能是生產(chǎn)者線程有可能是消費者線程。若僅剩一個元素時,某消費者線程執(zhí)行 pop 方法,判斷 if 條件不成立,執(zhí)行 notify 喚醒了另外的消費者線程,并消費了當(dāng)前的最后一個元素。被喚醒的消費者線程由于已經(jīng)在 if 方法中,不需要再判斷剩余的元素數(shù)量,又緊接著執(zhí)行了消費一個元素的操作,此時無元素可消費,程序就異常了。

          最后,我再補充下多消費者模式代碼中如果換成 while,且邏輯不正確時很容易發(fā)生程序掛起問題。

          因為使用 notify 仍存在導(dǎo)致程序掛起的風(fēng)險。這里先說一下對象的鎖池和等待池。執(zhí)行 wait 方法會使線程釋放鎖進(jìn)入鎖對象的等待池。notify 和 notifyAll 通知等待池中的線程,使其進(jìn)入鎖池競爭鎖資源。notify 僅僅通知等待池中的一個線程,使其進(jìn)入鎖池競爭鎖資源,若競爭到了鎖,線程就 running;notifyAll 會通知鎖對象的等待池中的所有線程進(jìn)入鎖池競爭鎖,盡管最后只能有一個線程得到鎖,剩下的都還等著鎖資源再釋放去競爭。還是舉多消費者的例子,若僅剩一個元素時,某消費者線程執(zhí)行 pop 方法,判斷 if 條件不成立,執(zhí)行 notify 喚醒了另外的消費者線程,并消費了當(dāng)前的最后一個元素。被喚醒的消費者線程由于已經(jīng)使用了 while 進(jìn)行優(yōu)化,會執(zhí)行 wait 操作釋放鎖并加入等待池。此時,若前面全部使用 notify,就會出現(xiàn)鎖池中沒有線程(都在等待池等著 notify/notifyAll),無人競爭已被釋放的鎖的情況,這樣所有線程都無法 running,程序就被掛起了。

          以上知識,如有疑問,歡迎加我微信:codedq,進(jìn)群溝通。如果覺得內(nèi)容還可以,歡迎點贊!

          瀏覽 98
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  爱操影视| 无码精品黑人 | 亚洲视频网站免费观看 | 三级片日韩在线观看 | AV日韩在线播放 |