1. 程式人生 > 程式設計 >Java併發程式設計入門(九)死鎖和死鎖定位

Java併發程式設計入門(九)死鎖和死鎖定位

Java極客  |  作者  /  鏗然一葉
這是Java極客的第 37 篇原創文章

一、死鎖條件

死鎖:一組互相競爭資源的執行緒因互相等待,導致“永久”阻塞的現象。

滿足死鎖的四個條件:
1.互斥,共享資源 X 和 Y 只能被一個執行緒佔用
2.佔有且等待,執行緒 T1 已經取得共享資源 X,在等待共享資源Y的時候,不釋放共享資源 X;
3.不可搶佔,其他執行緒不能強行搶佔執行緒 T1佔有的資源,因為不可搶佔,所以要等待;
4.迴圈等待,執行緒T1等待執行緒T2佔有的資源,執行緒T2等待執行緒T1佔有的資源,就是迴圈等待。

這四個條件同時滿足時,才會發生死鎖,因此避免死鎖只要打破其中一個條件則可。

二、避免死鎖方法

1.對於互斥這個條件無法破壞,因為使用鎖為的就是互斥。
2.對於佔有且等待,可以同時獲取要使用的多個資源鎖X和Y,這樣就不會存在取得了X還要等待Y。這種方式只在需要獲取的資源鎖較少的情況下使用,如果要獲取的資源鎖很多(例如10個),就不太可行。
3.對於不可搶佔,可以獲取了部分資源,再進一步獲取其他資源時如果獲取不到時,把已經獲取的資源一起釋放掉。此時意味著操作不能按照預期處理,需要考慮異常如何處理,例如是否需要重試。
4.對於迴圈等待,可以將需要獲取的鎖資源排序,按照順序獲取,這樣就不會多個執行緒交叉獲取相同的資源導致死鎖,而是在獲取相同的資源時就等待,直到它釋放。

綜上,對於極易發生死鎖的場景,處理如下:
1.獲取鎖時帶上超時時間,獲取不到就放棄,這樣能最簡單的避免死鎖,這也意味著不能使用synchronized關鍵字來獲得鎖資源。
2.對於已經獲取到的鎖資源,增加主動釋放機制。
3.放棄鎖資源時增加異常流程處理,如重試。
4.需要獲取的多個鎖資源排序處理,雖然前面幾點可以一定程度避免死鎖,但不排序的結果就是首次處理失敗,重試時還可能再次失敗,雖然沒有發生死鎖,但同一筆業務重試了N次可能也沒有成功,導致無謂佔用資源。

三、死鎖定位

1.模擬死鎖程式碼

package com.javashizhan.concurrent.demo.deadlock;

/**
 * @ClassName DeadlockDemo
 * @Description TODO
 * @Author 鏗然一葉
 * @Date 2019/10/3 23:40
 * javashizhan.com
 **/
public class DeadlockDemo {
    public static void main(String[] args) {
        //建立兩個用於加鎖的物件
        final Object lockX = new
Object(); final Object lockY = new Object(); System.out.println("lockX " + lockX); System.out.println("lockY " + lockY); Thread tX = new Thread(new Worker(lockX,lockY),"tX"); //交換鎖的順序,模擬死鎖 Thread tY = new Thread(new Worker(lockY,lockX),"tY"); tX.start(); tY.start(); } } class Worker implements Runnable { private final Object lockX; private final Object lockY; public Worker(Object lockX,Object lockY) { this.lockX = lockX; this.lockY = lockY; } public void run() { synchronized (lockX) { //休眠一會,等待另外一個執行緒獲取到lockY sleep(2000); System.out.println(Thread.currentThread().getName() + " get lock " + lockX); synchronized (lockY) { //這一步由於發生了死鎖永遠不會執行 System.out.println(Thread.currentThread().getName() + " get lock " + lockY); } } } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } } 複製程式碼

2.執行程式輸出的日誌

lockX java.lang.Object@28d93b30
lockY java.lang.Object@1b6d3586
tX get lock java.lang.Object@28d93b30
tY get lock java.lang.Object@1b6d3586
複製程式碼

可以看到兩個執行緒各自獲取一個鎖後發生了死鎖,沒有繼續往下執行。

3.jps檢視java程式

4.jstack檢視java程式堆疊資訊,關鍵部分如下:


可以看到有一個死鎖發生,原因是tY執行緒和tX執行緒已經獲取到的鎖和將要獲取的鎖形成了迴圈依賴,導致死鎖。

四、解決死鎖問題

對於這個例子,死鎖是因為兩個鎖迴圈依賴,根據上面描述的避免死鎖方法,只要對鎖排序則可,排序程式碼如下:

    public Worker(Object lockX,Object lockY) {
        int result = lockX.toString().compareTo(lockY.toString());
        this.lockX = result == -1 ? lockX : lockY;
        this.lockY = result == -1 ? lockY : lockX;
    }
複製程式碼

程式碼修改後程式執行日誌:

lockX java.lang.Object@28d93b30
lockY java.lang.Object@1b6d3586
tX get lock java.lang.Object@1b6d3586
tX get lock java.lang.Object@28d93b30
tY get lock java.lang.Object@1b6d3586
tY get lock java.lang.Object@28d93b30
複製程式碼

可以看到鎖排序後,只有一個執行緒獲取到所有鎖並執行完後,另外一個執行緒才能獲取鎖,死鎖問題解決。

end.


相關閱讀:
Java併發程式設計(一)知識地圖
Java併發程式設計(二)原子性
Java併發程式設計(三)可見性
Java併發程式設計(四)有序性
Java併發程式設計(五)建立執行緒方式概覽
Java併發程式設計入門(六)synchronized用法
Java併發程式設計入門(七)輕鬆理解wait和notify以及使用場景
Java併發程式設計入門(八)執行緒生命週期
Java併發程式設計入門(十)鎖優化
Java併發程式設計入門(十一)限流場景和Spring限流器實現
Java併發程式設計入門(十二)生產者和消費者模式-程式碼模板
Java併發程式設計入門(十三)讀寫鎖和快取模板
Java併發程式設計入門(十四)CountDownLatch應用場景
Java併發程式設計入門(十五)CyclicBarrier應用場景
Java併發程式設計入門(十六)秒懂執行緒池差別
Java併發程式設計入門(十七)一圖掌握執行緒常用類和介面
Java併發程式設計入門(十八)再論執行緒安全


Java極客站點: javageektour.com/