Java 併發程式設計:如何防止線上程阻塞與喚醒時死鎖

極客小智發表於2020-12-17

Java併發程式設計:多執行緒如何實現阻塞與喚醒 說到suspend與resume組合有死鎖傾向,一不小心將導致很多問題,甚至導致整個系統崩潰。接著看另外一種解決方案,我們可以使用以物件為目標的阻塞,即利用Object類的wait()和notify()方法實現執行緒阻塞。當執行緒到達監控物件時,通過wait方法會使執行緒進入到等待佇列中。而當其它執行緒呼叫notify時則可以使執行緒重新回到執行佇列中,得以繼續執行

 思維不同

針對物件的阻塞程式設計思維需要我們稍微轉變下思維,它與面向執行緒阻塞思維有較大差異。如前面的suspend與resume只需線上程內直接呼叫就能完成掛起恢復操作,這個很好理解。而如果改用wait與notify形式則是通過一個object作為訊號,可以將其看成是一堵門。object的wait()方法是鎖門的動作,notify()是開門的動作。某一執行緒一旦關上門後其他執行緒都將阻塞,直到別的執行緒開啟門。



如圖所示,一個物件object呼叫wait()方法則像是堵了一扇門。執行緒一、執行緒二都將阻塞,然後執行緒三呼叫object的notify()方法開啟門,準確地說是呼叫了notifyAll()方法,notify()僅僅能讓執行緒一或執行緒二其中一條執行緒通過)。最終執行緒一、執行緒二得以通過。

 死鎖問題解決了嗎?

使用wait與notify能在一定程度上避免死鎖問題,但並不能完全避免,它要求我們必須在程式設計過程中避免死鎖。在使用過程中需要注意的幾點是:

  • 首先,wait與notify方法是針對物件的,呼叫任意物件的wait()方法都將導致執行緒阻塞,阻塞的同時也將釋放該物件的鎖。相應地,呼叫任意物件的notify()方法則將隨機解除該物件阻塞的執行緒,但它需要重新獲取改物件的鎖,直到獲取成功才能往下執行。

  • 其次,wait與notify方法必須在synchronized塊或方法中被呼叫,並且要保證同步塊或方法的鎖物件與呼叫wait與notify方法的物件是同一個。如此一來在呼叫wait之前當前執行緒就已經成功獲取某物件的鎖,執行wait阻塞後當前執行緒就將之前獲取的物件鎖釋放。當然假如你不按照上面規定約束編寫,程式一樣能通過編譯,但執行時將丟擲IllegalMonitorStateException異常,必須在編寫時保證用法正確。

  • 最後,notify是隨機喚醒一條阻塞中的執行緒並讓之獲取物件鎖,進而往下執行,而notifyAll則是喚醒阻塞中的所有執行緒,讓他們去競爭該物件鎖,獲取到鎖的那條執行緒才能往下執行。

 改進例子

我們通過wait與notify改造前面的例子,程式碼如下。改造的思想就是在MyThread中新增一個標識變數,一旦變數改變就相應地呼叫wait和notify阻塞喚醒執行緒。由於在執行wait後將釋放synchronized(this)鎖住的物件鎖,此時System.out.println("running….");早已執行完畢,System類out物件不存在死鎖問題。





 Park與UnPark

wait與notify組合的方式看起來是個不錯的解決方式,但其面向的主體是物件object,阻塞的是當前執行緒,而喚醒的是隨機的某個執行緒或所有執行緒,偏重於執行緒之間的通訊互動。假如換個角度,面向的主體是執行緒的話,我就能輕而易舉地對指定的執行緒進行阻塞喚醒,這個時候就需要LockSupport,它提供的park與unpark方法分別用於阻塞和喚醒.而且它提供避免死鎖和競態條件,很好地代替suspend和resume組合。





用park與unpark改造上述例子,程式碼如下。把主體換成執行緒進行的阻塞看起來貌似比較順眼,而且由於park與unpark方法控制的顆粒度更加細小,能準確決定執行緒在某個點停止,進而避免死鎖的產生。例如此例中在執行System.out.println前執行緒就被阻塞了,於是不存在因競爭System類out物件而產生死鎖,即便在執行System.out.println後執行緒才阻塞也不存在死鎖問題,因為鎖已釋放。

 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則完全不用考慮物件、鎖、等待佇列等問題。

相關文章