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