Java併發編程入門(九)死鎖和死鎖定位

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

1、死鎖條件

死鎖:一組互相競爭資源的線程因互相等待,致使「永久」阻塞的現象。java

知足死鎖的四個條件:
1.互斥,共享資源 X 和 Y 只能被一個線程佔用
2.佔有且等待,線程 T1 已經取得共享資源 X,在等待共享資源Y的時候,不釋放共享資源 X;
3.不可搶佔,其餘線程不能強行搶佔線程 T1佔有的資源,由於不可搶佔,因此要等待;
4.循環等待,線程T1等待線程T2佔有的資源,線程T2等待線程T1佔有的資源,就是循環等待。編程

這四個條件同時知足時,纔會發生死鎖,所以避免死鎖只要打破其中一個條件則可。緩存

2、避免死鎖方法

1.對於互斥這個條件沒法破壞,由於使用鎖爲的就是互斥。
2.對於佔有且等待,能夠同時獲取要使用的多個資源鎖X和Y,這樣就不會存在取得了X還要等待Y。這種方式只在須要獲取的資源鎖較少的狀況下使用,若是要獲取的資源鎖不少(例如10個),就不太可行。
3.對於不可搶佔,能夠獲取了部分資源,再進一步獲取其餘資源時若是獲取不到時,把已經獲取的資源一塊兒釋放掉。此時意味着操做不能按照預期處理,須要考慮異常如何處理,例如是否須要重試。
4.對於循環等待,能夠將須要獲取的鎖資源排序,按照順序獲取,這樣就不會多個線程交叉獲取相同的資源致使死鎖,而是在獲取相同的資源時就等待,直到它釋放。安全

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

3、死鎖定位

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.執行程序輸出的日誌post

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進程this

4.jstack查看java進程堆棧信息,關鍵部分以下:spa


能夠看到有一個死鎖發生,緣由是tY線程和tX線程已經獲取到的鎖和將要獲取的鎖造成了循環依賴,致使死鎖。

4、解決死鎖問題

對於這個例子,死鎖是由於兩個鎖循環依賴,根據上面描述的避免死鎖方法,只要對鎖排序則可,排序代碼以下:

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/

相關文章
相關標籤/搜索