Java鎖——死鎖

gary-liu發表於2017-06-24

死鎖

死鎖是這樣一種情形:多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於執行緒被無限期地阻塞,因此程式不可能正常終止。

產生條件

java 死鎖產生的四個必要條件:

1、互斥使用,即當資源被一個執行緒使用(佔有)時,別的執行緒不能使用
2、不可搶佔,資源請求者不能強制從資源佔有者手中奪取資源,資源只能由資源佔有者主動釋放。
3、請求和保持,即當資源請求者在請求其他的資源的同時保持對原有資源的佔有。
4、迴圈等待,即存在一個等待佇列:P1佔有P2的資源,P2佔有P3的資源,P3佔有P1的資源。這樣就形成了一個等待環路。
當上述四個條件都成立的時候,便形成死鎖。當然,死鎖的情況下如果打破上述任何一個條件,便可讓死鎖消失。

死鎖示例

有兩個共享資源object1,object2,我們知道每個物件內部都有一個監視器鎖,當一個執行緒佔有資源 object1,保持不釋放,並申請資源 object2;而另一個執行緒佔有資源 object2,保持不釋放,並申請資源 object1;這樣就會導致兩個執行緒一直在迴圈等待,產生死鎖。

/**
 * ClassName: MyDeadLock <br/>
 * Function: 死鎖示例<br/>
 *
 * @author gary.liu
 * @date 2017/6/24
 */
public class MyDeadLock {

    private static Object object1 = new Object();
    private static Object object2 = new Object();

    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    static class Worker implements Runnable {

        public void run() {
            synchronized (object1) {

                System.out.println("Worker has object1 lock!");
                /**
                 *
                 * 不睡一會的話,由於兩個執行緒的執行時間可能不是同時的,就會有一個先執行,獲取兩個資源後釋放,一個後執行,此時可能不會產生死鎖;
                 * 如果執行緒幾乎同時開始執行,則就有可能產生死鎖,加這個 sleep 時間就是讓死鎖產生的更明顯.
                 *
                 * 為了證明上面的說法,使用 CountDownLatch 讓兩個執行緒同時執行來看看結果
                 *
                 */
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (object2) {
                    //由於產生死鎖,無法輸出
                    System.out.println("Worker has object2 lock!");
                }
            }


        }
    }

    static class Boss implements Runnable {

        public void run() {
            synchronized (object2) {

                System.out.println("Boss has object2 lock!");

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (object1) {
                    //由於產生死鎖,無法輸出
                    System.out.println("Boss has object1 lock!");

                }
            }
        }
    }

    public static void main(String[] args) throws Exception{

        Thread worker = new Thread(new Worker());
        Thread boss = new Thread(new Boss());

        worker.start();
        boss.start();

    }

}

執行結果

Worker has object1 lock!
Boss has object2 lock!
//兩個執行緒一直在迴圈等待獲取對方資源,產生死鎖,程式還一直在執行,但沒有內容輸出了

程式碼中的註釋:
不睡一會的話,由於兩個執行緒的執行時間可能不是同時的,就會有一個先執行,一個後執行,此時可能不會產生死鎖;如果執行緒幾乎同時開始執行,則就有可能產生死鎖,加這個等待時間就是讓死鎖更有機會產生. 為了證明上面的說法,使用 CountDownLatch 讓兩個執行緒同時執行來看看結果,不用 sleep 一會,基本每次都會產生死鎖。

兩個執行緒同時執行,加大產生死鎖的機會

public class MyDeadLock {

    private static Object object1 = new Object();
    private static Object object2 = new Object();

    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    static class Worker implements Runnable {

        public void run() {
            synchronized (object1) {

                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }

                System.out.println("Worker has object1 lock!");

                synchronized (object2) {
                    //由於產生死鎖,無法輸出
                    System.out.println("Worker has object2 lock!");
                }
            }
        }
    }

    static class Boss implements Runnable {

        public void run() {
            synchronized (object2) {

                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("Boss has object2 lock!");

                synchronized (object1) {
                    //由於產生死鎖,無法輸出
                    System.out.println("Boss has object1 lock!");

                }
            }
        }
    }

    public static void main(String[] args) throws Exception{

        Thread worker = new Thread(new Worker());
        Thread boss = new Thread(new Boss());

       //因為用countDownLatch.await();兩個執行緒被阻塞了
        worker.start();
        boss.start();

        //更清楚的看到 worker,boss 執行緒被阻塞
       Thread.sleep(3000); 
       //之後兩個執行緒開始同時繼續執行,基本每次都會產生死鎖
       countDownLatch.countDown();  
    }

}

分析死鎖

1、jps 檢視程式程式 id,即 pid
2、jstack pid 列印堆疊資訊,可以從堆疊資訊中看到 Thread-1 和 Thread-0 在互相等待對方釋放資源。
這裡寫圖片描述

具體分析參考:Java多執行緒7:死鎖

或者用 jdk 自帶的命令 jvisualvm 工具,對程式的執行進行監控,可以看到執行緒監控中已經檢測到死鎖,如下圖。
這裡寫圖片描述

避免死鎖

破壞上面產生死鎖的四個必要條件

遵循以下原則有助於規避死鎖:

1、只在必要的最短時間內持有鎖,考慮使用同步語句塊代替整個同步方法;
2、儘量編寫不在同一時刻需要持有多個鎖的程式碼,如果不可避免,則確保執行緒持有第二個鎖的時間儘量短暫;
3、建立和使用一個大鎖來代替若干小鎖,並把這個鎖用於互斥,而不是用作單個物件的物件級別鎖;
4、破壞迴圈等待條件,可以使用 Lock 類中的 tryLock 方法去嘗試獲取鎖,這個方法可以指定一個超時時限,在等待超過該時限之後變回返回一個失敗資訊;也可以使用訊號量,指定去獲取的超時時間。

參考資料

Java 例項 - 死鎖及解決方法
Java多執行緒7:死鎖

相關文章