一步一步掌握線程機制(五)---等待與通知機制


      在之前我們關於停止Thread的討論中,曾經使用過設定標記done的做法,一旦done設置為true,線程就會結束,一旦為false,線程就會永遠運行下去。這樣做法會消耗掉許多CPU循環,是一種對內存不友好的行為。

      java中的對象不僅擁有鎖,而且它們本身就可以通過調用相關方法使自己成為等待者和通知者。

     Object對象本身有兩個方法:wait()和notify()。wait()會等待條件的發生,而notify()會通知正在等待的線程此條件已經發生,它們都必須從synchronized方法或塊中調用。

     這種等待-通知機制的目的究竟是為何?

     等待-通知機制是一種同步機制,但它更像是一個通信機制,能夠讓一個線程與另一個線程在某個特定條件下進行通信。但是,該機制卻沒有指定特定條件是什么。

     等待-通知機制能否取代synchronized機制嗎?當然不行,等待-通知機制並不會解決synchronized機制能夠解決的競爭問題,實際上,這兩者是相互配合使用的,而且它本身也存在競爭問題,這是需要通過synchronzied來解決的。

private boolean done = true;

public synchronized void run(){
      while(true){
            try{
                 if(done){
                       wait();
                 }else{
                       repaint();
                       wait(100);
                 }
            }catch(InterruptedException e){
                  return;
            }
      }
}

public synchronized void setDone(boolean b){
     done = b;
     if(timer == null){
          timer = new Thread(this);
          timer.start();
     }
     if(!done){
          notify();
     }
}

     這里的done已經不是volatile,因為我們不只是設定個標記值,我們還需要在設定標記的同時自動發送一個通知。所以,我們現在是通過synchronized來保護對done的訪問。
     run()方法不會在done為false時自動退出,它會通過調用wait()方法讓線程在這個方法中等待,直到其他線程調用notify()方法。

     這里有幾個地方值得我們注意。

     首先,我們這里通過使用wait()方法而不是sleep()方法來使線程休眠,因為wait()方法需要線程持有該對象的同步鎖,當wait()方法執行的時候,該鎖就會被釋放,而當收到通知的時候,線程需要在wait()方法返回前重新獲得該鎖,就好像一直都持有鎖一樣。這個技巧是因為在設定與發送通知以及測試與取得通知之間是存在競爭的,如果wait()和notify()在持有同步鎖的同時沒有被調用,是完全沒有辦法保證此通知會被接收到的,並且如果wait()方法在等待前沒有釋放掉鎖,是不可能讓notify()方法被調用到,因為它無法取得鎖,這也是我們之所以使用wait()而不是sleep()的另一個原因。如果使用sleep()方法,此鎖就永遠不會被釋放,setDone()方法也永遠不會執行,通知也永遠不會送出。

     接着就是這里我們對run()進行同步化。我們之前討論過,對run()進行同步是非常危險的,因為run()方法是絕對不可能會完成的,也就是鎖永遠不會被釋放,但是因為wait()本身就會釋放掉鎖,所以這個問題也被避免了。

     我們會有一個疑問:如果在notify()方法被調用的時候,沒有線程在等待呢?

     等待-通知機制並不知道所送出通知的條件,它會假設通知在沒有線程等待的時候是沒有被收到的,因為這時它也只是返回且通知也被遺失掉,稍后執行wait()方法的線程就必須等待另一個通知。

     上面我們講過,等待-通知機制本身也存在競爭問題,這真是一個諷刺:原本用來解決同步問題的機制本身竟然也存在同步問題!其實,競爭並不一定是個問題,只要它不引發問題就行。我們現在就來分析一下這里的競爭問題:

     使用wait()的線程會確認條件不存在,這通常是通過檢查變量實現的,然后我們才調用wait()方法。當其他線程設立了該條件,通常也是通過設定同一個變量,才會調用notify()方法。競爭是發生在下列幾種情況:

1.第一個線程測試條件並確認它需要等待;

2.第二個線程設定此條件;

3.第二個線程調用notify()方法,這並不會被收到,因為第一個線程還沒有進入等待;

4.第一個線程調用wait()方法。

      這種競爭就需要同步鎖來實現。我們必須取得鎖以確保條件的檢查和設定都是automic,也就是說檢查和設定都必須處於鎖的范圍內。

      既然我們上面講到,wait()方法會釋放鎖然后重新獲取鎖,那么是否會有競爭是發生在這段期間呢?理論上是會有,但系統會阻止這種情況。wait()方法與鎖機制是緊密結合的,在等待的線程還沒有進入准備好可以接收通知的狀態前,對象的鎖實際上是不會被釋放的。

      我們的疑問還在繼續:線程收到通知,是否就能保證條件被正確的設定呢?抱歉,答案不是。在調用wait()方法前,線程永遠應該在持有同步鎖時測試條件,在從wait()方法返回時,該線程永遠應該重新測試條件以判斷是否還需要等待,這是因為其他的線程同樣也能夠測試條件並判斷出無需等待,然后處理由發出通知的線程所設定的有效數據。但這是在只有一個線程在等待通知,如果是多個線程在等待通知,就會發生競爭,而且這是等待-通知機制所無法解決的,因為它能解決的只是內部的競爭以防止通知的遺失。多線程等待最大的問題就是,當一個線程在其他線程收到通知后再收到通知,它無法保證這個通知是有效的,所以等待的線程必須提供選項以供檢查狀態,並在通知已經被處理的情形下返回到等待的狀態,這也是我們為什么總是要將wait()放在循環里面的原因。

      wait()也會在它的線程被中斷時提前返回,我們的程序也必須要處理該中斷。

      在多線程通知中,我們如何確保正確的線程收到通知呢?答案是不行的,因為我們根本就無法保證哪一個線程能夠收到通知,能夠做到的方法就是所有等待的線程都會收到通知,這是通過notifyAll()實現的,但也不是真正的喚醒所有等待的線程,因為鎖的問題,實質上所有的線程都會被喚醒,但是真正在執行的線程只有一個。

       之所以要這樣做,可能是因為有一個以上的條件要等待,既然我們無法確保哪一個線程會被喚醒,那就干脆喚醒所有線程,然后由它們自己根據條件判斷是否要執行。

       等待-通知機制可以和synchronized結合使用:

private Object doneLock = new Object();

public void run(){
     synchronized(doneLock){
           while(true){
                if(done){
                      doneLock.wait();
                }else{
                      repaint();
                      doneLock.wait(100);
                }
           }catch(InterruptedException e){
                 return;
           }
     }
}

public void setDone(boolean b){
     synchronized(doneLock){
          done = b;
          if(timer == null){
               timer = new Thread(this);
               timer.start();
          }
          if(!done){
                doneLock.notify();
          }
     }
}

     這個技巧是非常有用的,尤其是在具有許多對對象鎖的競爭中,因為它能夠在同一時間內讓更多的線程去訪問不同的方法。
     最后我們要介紹的是條件變量。

     J2SE5.0提供了Condition接口。Condition接口是綁定在Lock接口上的,就像等待-通知機制是綁定在同步鎖上一樣。

private Lock lock = new ReentrantLock();
private Condition  cv = lockvar.newCondition();

public void run(){
     try{
          lock.lock();
          while(true){
               try{
                   if(done){
                         cv.await();
                   }else{
                         nextCharacter();
                         cv.await(getPauseTime(), TimeUnit.MILLISECONDS);
                   }
               }catch(InterruptedException e){
                     return;
               }
          }
     }finally{
           lock.unlock();
     }
}

public void setDone(boolean b){
    try{
         lock.lock();
         done = b;
         if(!done){
               cv.signal();
         }finally{
               lock.unlock();
         }
    }
}

    上面的例子好像是在使用另一種方式來完成我們之前的等待-通知機制,實際上使用條件變量是有幾個理由的:
1.條件變量在使用Lock對象時是必須的,因為Lock對象的wait()和notify()是無法運作的,因為這些方法已經在內部被用來實現Lock對象,更重要的是,持有Lock對象並不表示持有該對象的同步鎖,因為Lock對象和對象所關聯的同步鎖是不同的。

2.Condition對象不像java的等待-通知機制,它是被創建成不同的對象,對每個Lock對象都可以創建一個以上的Condition對象,於是我們可以針對個別的線程或者一群線程進行獨立的設定,也就是說,對同一個對象上所有被同步化的在等待的線程都得等待相同的條件。

      基本上,Condition接口的方法都是復制等待-通知機制,但是提供了避免被中斷或者能以相對或絕對時間來指定時限的便利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM