Java 併發程式設計:如何防止線上程阻塞與喚醒時死鎖
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則完全不用考慮物件、鎖、等待佇列等問題。
相關文章
- 併發程式設計之臨界區\阻塞\非阻塞\死鎖\飢餓\活鎖程式設計
- Java併發程式設計實戰(4)- 死鎖Java程式設計
- 併發程式設計喚醒判斷用while程式設計While
- Java併發程式設計——阻塞佇列Java程式設計佇列
- Java併發程式設計:阻塞佇列Java程式設計佇列
- Java併發程式設計實戰 04死鎖了怎麼辦?Java程式設計
- Java併發程式設計-鎖及併發容器Java程式設計
- [Java併發]避免死鎖Java
- 線上併發事務死鎖問題排查
- 併發程式設計中死鎖、遞迴鎖、程式/執行緒池、協程TCP伺服器併發等知識點程式設計遞迴執行緒TCP伺服器
- 併發程式設計之 Java 三把鎖程式設計Java
- Java併發程式設計-讀寫鎖(ReentrantReadWriteLock)Java程式設計
- 併發:死鎖
- Java併發程式設計——深入理解自旋鎖Java程式設計
- 《JAVA併發程式設計實戰》顯式鎖Java程式設計
- Java併發程式設計---java規範與模式下的併發程式設計1.1Java程式設計模式
- java如何避免程式死鎖Java
- Java併發程式設計:執行緒和鎖的使用與解析Java程式設計執行緒
- Java併發程式設計之美-千無萬喚使出來Java程式設計
- Java併發程式設計之鎖機制之(ReentrantLock)重入鎖Java程式設計ReentrantLock
- Java併發——阻塞佇列集(上)Java佇列
- 【java併發程式設計】ReentrantLock 可重入讀寫鎖Java程式設計ReentrantLock
- Java併發程式設計之鎖機制之AQSJava程式設計AQS
- Python | 淺談併發鎖與死鎖問題Python
- Java併發程式設計之鎖機制之ReentrantReadWriteLock(讀寫鎖)Java程式設計
- java併發程式設計系列:java併發程式設計背景知識Java程式設計
- 併發程式設計之死鎖解析程式設計
- 解決庫存扣減及訂單建立時防止併發死鎖的問題
- Java併發程式設計Java程式設計
- java 併發程式設計Java程式設計
- Java併發程式設計中的鎖機制詳解Java程式設計
- java併發程式設計-StampedLock高效能讀寫鎖Java程式設計
- Java 併發程式設計(十一) -- ReentrantLock中的公平鎖FairSyncJava程式設計ReentrantLockAI
- java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLockJava程式設計AQSReentrantLock
- Java併發程式設計之鎖機制之Condition介面Java程式設計
- Java併發程式設計之鎖機制之LockSupport工具Java程式設計
- Java併發程式設計之鎖機制之Lock介面Java程式設計
- Java併發程式設計(05):悲觀鎖和樂觀鎖機制Java程式設計