Java多執行緒7:死鎖

五月的倉頡發表於2015-10-03

前言

死鎖單獨寫一篇文章是因為這是一個很嚴重的、必須要引起重視的問題。這不是誇大死鎖的風險,儘管鎖被持有的時間通常很短,但是作為商業產品的應用程式每天可能要執行數十億次獲取鎖->釋放鎖的操作,只要在這數十億次操作中只要有一次發生了錯誤,就可能導致程式中發生死鎖,並且即使通過壓力測試也不可能找出所有潛在的死鎖。

 

死鎖

一個經典的多執行緒問題。

當一個執行緒永遠地持有一個鎖,並且其他執行緒都嘗試去獲得這個鎖時,那麼它們將永遠被阻塞,這個我們都知道。如果執行緒A持有鎖L並且想獲得鎖M,執行緒B持有鎖M並且想獲得鎖L,那麼這兩個執行緒將永遠等待下去,這種情況就是最簡單的死鎖形式。

在資料庫系統的設計中考慮了監測死鎖以及從死鎖中恢復,資料庫如果監測到了一組事務發生了死鎖時,將選擇一個犧牲者並放棄這個事務。Java虛擬機器解決死鎖問題方面並沒有資料庫這麼強大,當一組Java執行緒發生死鎖時,這兩個執行緒就永遠不能再使用了,並且由於兩個執行緒分別持有了兩個鎖,那麼這兩段同步程式碼/程式碼塊也無法再執行了----除非終止並重啟應用。

死鎖是設計的BUG,問題比較隱晦。不過死鎖造成的影響很少會立即顯現出來,一個類可能發生死鎖,並不意味著每次都會發生死鎖,這只是表示有可能。當死鎖出現時,往往是在最糟糕的情況----高負載的情況下

下面給出一個產生死鎖的簡單程式碼並且演示如何分析這是一個死鎖:

public class DeadLock
{
    private final Object left = new Object();
    private final Object right = new Object();
    
    public void leftRight() throws Exception
    {
        synchronized (left)
        {
            Thread.sleep(2000);
            synchronized (right)
            {
                System.out.println("leftRight end!");
            }
        }
    }
    
    public void rightLeft() throws Exception
    {
        synchronized (right)
        {
            Thread.sleep(2000);
            synchronized (left)
            {
                System.out.println("rightLeft end!");
            }
        }
    }
}

注意這裡一定要有"Thread.sleep(2000)"讓執行緒睡一覺,不然一個執行緒執行了,另一個執行緒還沒有執行,先執行的執行緒很有可能就已經連續獲得兩個鎖了。寫兩個執行緒分別呼叫它們:

public class Thread0 extends Thread
{
    private DeadLock dl;
    
    public Thread0(DeadLock dl)
    {
        this.dl = dl;
    }
    
    public void run()
    {
        try
        {
            dl.leftRight();
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}
public class Thread1 extends Thread
{
    private DeadLock dl;
    
    public Thread1(DeadLock dl)
    {
        this.dl = dl;
    }
    
    public void run()
    {
        try
        {
            dl.rightLeft();
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

寫個main函式呼叫一下:

public static void main(String[] args)
{
    DeadLock dl = new DeadLock();
    Thread0 t0 = new Thread0(dl);
    Thread1 t1 = new Thread1(dl);
    t0.start();
    t1.start();

    while(true);   
}

至於結果,沒有結果,什麼語句都不會列印,因為死鎖了。下面演示一下如何定位死鎖問題:

1、jps獲得當前Java虛擬機器程式的pid

 

2、jstack列印堆疊。jstack列印內容的最後其實已經報告發現了一個死鎖,但因為我們是分析死鎖產生的原因,而不是直接得到這裡有一個死鎖的結論,所以別管它,就看前面的部分

先說明介紹一下每一部分的意思,以"Thread-1"為例:

(1)"Thread-1"表示執行緒名稱

(2)"prio=6"表示執行緒優先順序

(3)"tid=00000000497cec00"表示執行緒Id

(4)nid=0x219c

執行緒對應的本地執行緒Id,這個重點說明下。因為Java執行緒是依附於Java虛擬機器中的本地執行緒來執行的,實際上是本地執行緒在執行Java執行緒程式碼,只有本地執行緒才是真正的執行緒實體。Java程式碼中建立一個thread,虛擬機器在執行期就會建立一個對應的本地執行緒,而這個本地執行緒才是真正的執行緒實體。Linux環境下可以使用"top -H -p JVM程式Id"來檢視JVM程式下的本地執行緒(也被稱作LWP)資訊,注意這個本地執行緒是用十進位制表示的,nid是用16進製表示的,轉換一下就好了,0x219c對應的本地執行緒Id應該是8604。

(5)"[0x000000004a3bf000..0x000000004a3bf790]"表示執行緒佔用的記憶體地址

(6)"java.lang.Thread.State:BLOCKED"表示執行緒的狀態

解釋完了每一部分的意思,看下Thread-1處於BLOCKED狀態,Thread-0處於BLOCKED狀態。對這兩個執行緒分析一下:

(1)Thread-1獲得了鎖0x000000003416a4e8,在等待鎖0x000000003416a4d8

(2)Thread-0獲得了鎖0x000000003416a4d8,在等待鎖0x000000003416a4e8

由於兩個執行緒都在等待獲取對方持有的鎖,所以就這麼永久等待下去了。

3、注意一下使用Eclipse/MyEclipse,這段程式如果不點選控制檯上面的紅色方框去Terminate掉它,而是右鍵->Run As->1 Java Application的話,這個程式會一直存在的,這時候可以利用taskkill命令去終止沒有被Terminate的程式:

 

避免死鎖的方式

既然可能產生死鎖,那麼接下來,講一下如何避免死鎖。

1、讓程式每次至多隻能獲得一個鎖。當然,在多執行緒環境下,這種情況通常並不現實

2、設計時考慮清楚鎖的順序,儘量減少嵌在的加鎖互動數量

3、既然死鎖的產生是兩個執行緒無限等待對方持有的鎖,那麼只要等待時間有個上限不就好了。當然synchronized不具備這個功能,但是我們可以使用Lock類中的tryLock方法去嘗試獲取鎖,這個方法可以指定一個超時時限,在等待超過該時限之後變回返回一個失敗資訊

相關文章