Java多線程中的死鎖問題

Java程序基本都要涉及到多線程,而在多線程環境中不可避免的要遇到線程死鎖的問題。Java不像數據庫那麼可以檢測到死鎖,而後進行處理,Java中的死鎖問題,只能經過程序員本身寫代碼時避免引入死鎖的可能性來解決。程序員

1. Java中致使死鎖的緣由數據庫

Java中死鎖最簡單的狀況是,一個線程T1持有鎖L1而且申請得到鎖L2,而另外一個線程T2持有鎖L2而且申請得到鎖L1,由於默認的鎖申請操做都是阻塞的,因此線程T1和T2永遠被阻塞了。致使了死鎖。這是最容易理解也是最簡單的死鎖的形式。可是實際環境中的死鎖每每比這個複雜的多。可能會有多個線程造成了一個死鎖的環路,好比:線程T1持有鎖L1而且申請得到鎖L2,而線程T2持有鎖L2而且申請得到鎖L3,而線程T3持有鎖L3而且申請得到鎖L1,這樣致使了一個鎖依賴的環路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而致使了死鎖。編程

從這兩個例子,咱們能夠得出結論,產生死鎖可能性的最根本緣由是:線程在得到一個鎖L1的狀況下再去申請另一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在得到了鎖L1,而且沒有釋放鎖L1的狀況下,又去申請得到鎖L2,這個是產生死鎖的最根本緣由。另外一個緣由是默認的鎖申請操做是阻塞的多線程

2. Java中如何避免死鎖併發

既然咱們知道了產生死鎖可能性的緣由,那麼就能夠在編碼時進行規避。Java是面向對象的編程語言,程序的最小單元是對象,對象封裝了數據和操做,因此Java中的鎖通常也是以對象爲單位的,對象的內置鎖保護對象中的數據的併發訪問。因此若是咱們可以避免在對象的同步方法中調用其它對象的同步方法,那麼就能夠避免死鎖產生的可能性。以下所示的代碼,就存在死鎖的可能性:編程語言

public class ClassB {
    private String address;
    // ...
    
    public synchronized void method1(){
        // do something
    }
// ... ... }
public class ClassA {
    private int id;
    private String name;
    private ClassB b;
    // ...
    
    public synchronized void m1(){
        // do something
        b.method1();
    }
    // ... ... }

上面的ClassA.m1()方法,在對象的同步方法中又調用了ClassB的同步方法method1(),因此存在死鎖發生的可能性。咱們能夠修改以下,避免死鎖:this

public class ClassA {
    private int id;
    private String name;
    private ClassB b;
    // ...
    
    public void m2(){
        synchronized(this){
            // do something
        }
        b.method1();
    }
    // ... ...
}

這樣的話減少了鎖定的範圍,兩個鎖的申請就沒有發生交叉,避免了死鎖的可能性,這是最理性的狀況,由於鎖沒有發生交叉。可是有時是不容許咱們這樣作的。此時,若是隻有ClassA中只有一個m1這樣的方法,須要同時得到兩個對象上的鎖,而且不會將實例屬性 b 溢出(return b;),而是將實例屬性 b 封閉在對象中,那麼也不會發生死鎖。由於沒法造成死鎖的閉環。可是若是ClassA中有多個方法須要同時得到兩個對象上的鎖,那麼這些方法就必須以相同的順序得到鎖。編碼

好比銀行轉帳的場景下,咱們必須同時得到兩個帳戶上的鎖,才能進行操做,兩個鎖的申請必須發生交叉。這時咱們也能夠打破死鎖的那個閉環,在涉及到要同時申請兩個鎖的方法中,老是以相同的順序來申請鎖,好比老是先申請 id 大的帳戶上的鎖 ,而後再申請 id 小的帳戶上的鎖,這樣就沒法造成致使死鎖的那個閉環。spa

public class Account {
    private int id;    // 主鍵
    private String name;
    private double balance;
    
    public void transfer(Account from, Account to, double money){
        if(from.getId() > to.getId()){
            synchronized(from){
                synchronized(to){
                    // transfer
                }
            }
        }else{
            synchronized(to){
                synchronized(from){
                    // transfer
                }
            }
        }
    }

    public int getId() {
        return id;
    }
}

這樣的話,即便發生了兩個帳戶好比 id=1的和id=100的兩個帳戶相互轉帳,由於不論是哪一個線程先得到了id=100上的鎖,另一個線程都不會去得到id=1上的鎖(由於他沒有得到id=100上的鎖),只能是哪一個線程先得到id=100上的鎖,哪一個線程就先進行轉帳。這裏除了使用id以外,若是沒有相似id這樣的屬性能夠比較,那麼也可使用對象的hashCode()的值來進行比較。線程

上面咱們說到,死鎖的另外一個緣由是默認的鎖申請操做是阻塞的,因此若是咱們不使用默認阻塞的鎖,也是能夠避免死鎖的。咱們可使用ReentrantLock.tryLock()方法,在一個循環中,若是tryLock()返回失敗,那麼就釋放以及得到的鎖,並睡眠一小段時間。這樣就打破了死鎖的閉環。

好比:線程T1持有鎖L1而且申請得到鎖L2,而線程T2持有鎖L2而且申請得到鎖L3,而線程T3持有鎖L3而且申請得到鎖L1

此時若是T3申請鎖L1失敗,那麼T3釋放鎖L3,並進行睡眠,那麼T2就能夠得到L3了,而後T2執行完以後釋放L2, L3,因此T1也能夠得到L2了執行完而後釋放鎖L1, L2,而後T3睡眠醒來,也能夠得到L1, L3了。打破了死鎖的閉環。

這些狀況,都仍是比較好處理的,由於它們都是相關的,咱們很容易意識到這裏有發生死鎖的可能性,從而能夠加以防備。不少狀況的場景都不會很明顯的讓咱們察覺到會存在發生死鎖的可能性。因此咱們仍是要注意:

一旦咱們在一個同步方法中,或者說在一個鎖的保護的範圍中,調用了其它對象的方法時,就要十而分的當心

1)若是其它對象的這個方法會消耗比較長的時間,那麼就會致使鎖被咱們持有了很長的時間;

2)若是其它對象的這個方法是一個同步方法,那麼就要注意避免發生死鎖的可能性了;

最好是可以避免在一個同步方法中調用其它對象的延時方法和同步方法。若是不能避免,就要採起上面說到的編碼技巧,打破死鎖的閉環,防止死鎖的發生。同時咱們還能夠儘可能使用「不可變對象」來避免鎖的使用,在某些狀況下還能夠避免對象的共享,好比 new 一個新的對象代替共享的對象,由於鎖通常是對象上的,對象不相同了,也就能夠避免死鎖,另外儘可能避免使用靜態同步方法,由於靜態同步至關於全局鎖。還有一些封閉技術可使用:好比堆棧封閉,線程封閉,ThreadLocal,這些技術能夠減小對象的共享,也就減小了死鎖的可能性。

總結一下

     死鎖的根本緣由1)是多個線程涉及到多個鎖,這些鎖存在着交叉,因此可能會致使了一個鎖依賴的閉環;2)默認的鎖申請操做是阻塞的因此要避免死鎖,就要在一遇到多個對象鎖交叉的狀況,就要仔細審查這幾個對象的類中的全部方法,是否存在着致使鎖依賴的環路的可能性。要採起各類方法來杜絕這種可能性。

相關文章
相關標籤/搜索