程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java線程機制(五) 等待與通知機制

Java線程機制(五) 等待與通知機制

編輯:關於JAVA

在之前我們關於停止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接口的方法都是復制等待-通知機制,但是提供了避免被中斷或者能以相對或絕對 時間來指定時限的便利。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved