java 多線程 - 死鎖

前言java

死鎖單獨寫一篇文章是由於這是一個很嚴重的、必需要引發重視的問題。這不是誇大死鎖的風險,儘管鎖被持有的時間一般很短,可是做爲商業產品的應用程序天天可能要執行數十億次獲取鎖->釋放鎖的操做,只要在這數十億次操做中只要有一次發生了錯誤,就可能致使程序中發生死鎖,而且即便經過壓力測試也不可能找出全部潛在的死鎖。數據庫

 

死鎖多線程

一個經典的多線程問題。函數

當一個線程永遠地持有一個鎖,而且其餘線程都嘗試去得到這個鎖時,那麼它們將永遠被阻塞,這個咱們都知道。若是線程A持有鎖L而且想得到鎖M,線程B持有鎖M而且想得到鎖L,那麼這兩個線程將永遠等待下去,這種狀況就是最簡單的死鎖形式。測試

在數據庫系統的設計中考慮了監測死鎖以及從死鎖中恢復,數據庫若是監測到了一組事物發生了死鎖時,將選擇一個犧牲者並放棄這個事物。Java虛擬機解決死鎖問題方面並無數據庫這麼強大,當一組Java線程發生死鎖時,這兩個線程就永遠不能再使用了,而且因爲兩個線程分別持有了兩個鎖,那麼這兩段同步代碼/代碼塊也沒法再運行了----除非終止並重啓應用。this

死鎖是設計的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函數調用一下:ip

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);   
}

 

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

一、jps得到當前Java虛擬機進程的pid

 

二、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

因爲兩個線程都在等待獲取對方持有的鎖,因此就這麼永久等待下去了。

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

 

避免死鎖的方式

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

一、讓程序每次至多隻能得到一個鎖。固然,在多線程環境下,這種狀況一般並不現實

二、設計時考慮清楚鎖的順序,儘可能減小嵌在的加鎖交互數量

三、既然死鎖的產生是兩個線程無限等待對方持有的鎖,那麼只要等待時間有個上限不就行了。固然synchronized不具有這個功能,可是咱們可使用Lock類中的tryLock方法去嘗試獲取鎖,這個方法能夠指定一個超時時限,在等待超過該時限以後變回返回一個失敗信息

相關文章
相關標籤/搜索