Java並發編程:多線程如何實現阻塞與喚醒說到suspend與resume組合有死鎖傾向,一不小心將導致很多問題,甚至導致整個系統崩潰。接著看另外一種解決方案,我們可以使用以對象為目標的阻塞,即利用Object類的wait()和notify()方法實現線程阻塞。當線程到達監控對象時,通過wait方法會使線程進入到等待隊列中。而當其它線程調用notify時則可以使線程重新回到執行隊列中,得以繼續執行
01
思維不同
針對對象的阻塞編程思維需要我們稍微轉變下思維,它與面向線程阻塞思維有較大差異。如前面的suspend與resume只需在線程內直接調用就能完成掛起恢復操作,這個很好理解。而如果改用wait與notify形式則是通過一個object作為信號,可以將其看成是一堵門。object的wait()方法是鎖門的動作,notify()是開門的動作。某一線程一旦關上門後其他線程都將阻塞,直到別的線程打開門。
如圖所示,一個對象object調用wait()方法則像是堵了一扇門。線程一、線程二都將阻塞,然後線程三調用object的notify()方法打開門,準確地說是調用了notifyAll()方法,notify()僅僅能讓線程一或線程二其中一條線程通過)。最終線程一、線程二得以通過。
02
死鎖問題解決了嗎?
使用wait與notify能在一定程度上避免死鎖問題,但並不能完全避免,它要求我們必須在編程過程中避免死鎖。在使用過程中需要注意的幾點是:
首先,wait與notify方法是針對對象的,調用任意對象的wait()方法都將導致線程阻塞,阻塞的同時也將釋放該對象的鎖。相應地,調用任意對象的notify()方法則將隨機解除該對象阻塞的線程,但它需要重新獲取改對象的鎖,直到獲取成功才能往下執行。
其次,wait與notify方法必須在synchronized塊或方法中被調用,並且要保證同步塊或方法的鎖對象與調用wait與notify方法的對象是同一個。如此一來在調用wait之前當前線程就已經成功獲取某對象的鎖,執行wait阻塞後當前線程就將之前獲取的對象鎖釋放。當然假如你不按照上面規定約束編寫,程序一樣能通過編譯,但運行時將拋出IllegalMonitorStateException異常,必須在編寫時保證用法正確。
最後,notify是隨機喚醒一條阻塞中的線程並讓之獲取對象鎖,進而往下執行,而notifyAll則是喚醒阻塞中的所有線程,讓他們去競爭該對象鎖,獲取到鎖的那條線程才能往下執行。
03
改進例子
04
Park與UnPark
wait與notify組合的方式看起來是個不錯的解決方式,但其面向的主體是對象object,阻塞的是當前線程,而喚醒的是隨機的某個線程或所有線程,偏重於線程之間的通信交互。假如換個角度,面向的主體是線程的話,我就能輕而易舉地對指定的線程進行阻塞喚醒,這個時候就需要LockSupport,它提供的park與unpark方法分別用於阻塞和喚醒.而且它提供避免死鎖和競態條件,很好地代替suspend和resume組合。
05
LockSupport優勢
LockSupport類為線程阻塞喚醒提供了基礎,同時,在競爭條件問題上具有wait和notify無可比擬的優勢。使用wait和notify組合時,某一線程在被另一線程notify之前必須要保證此線程已經執行到wait等待點,錯過notify則可能永遠都在等待,另外notify也不能保證喚醒指定的某線程。反觀LockSupport,由於park與unpark引入了許可機制,許可邏輯為:
park將許可在等於0的時候阻塞,等於1的時候返回並將許可減為0。
unpark嘗試喚醒線程,許可加1。
根據這兩個邏輯,對於同一條線程,park與unpark先後操作的順序似乎並不影響程序正確地執行。假如先執行unpark操作,許可則為1,之後再執行park操作,此時因為許可等於1直接返回往下執行,並不執行阻塞操作。 最後,LockSupport的park與unpark組合真正解耦了線程之間的同步,不再需要另外的對象變量存儲狀態,並且也不需要考慮同步鎖,wait與notify要保證必須有鎖才能執行,而且執行notify操作釋放鎖後還要將當前線程扔進該對象鎖的等待隊列,LockSupport則完全不用考慮對象、鎖、等待隊列等問題。
- END -