發生死鎖怎麼辦

鎖的定義:死鎖是指兩個或兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。競爭的資源能夠是:鎖、網絡鏈接、磁盤共享變量等一切能夠稱做是 【資源】的東西。java

咱們使用鎖來保證線程安全,可是使用不當與濫用可能就會引發死鎖。併發程序一旦死鎖,通常沒有特別好的辦法,不少時候只能重啓。因此咱們必定要比避免死鎖。數據庫

簡單例子

舉個不恰當的例子:如今嶽不羣經過陰謀手段獲取到了葵花寶典的上冊,而後就閉關修煉自宮了,此刻他想繼續爭奪下冊一塊練,否則自宮就白忙活了。這個時候下冊被林平之拿到了,他也要修煉葵花寶典,因此藏着下冊去找上冊來自宮。如今問題來了,嶽不羣找不到下冊。林平之拿不到上冊,兩我的就只能乾瞪眼誰也不願交出本身的,同事還要獲取對方的。安全

若是此時有一個線程 A ,按照先獲持有鎖 a 再獲取鎖 b的順序得到鎖,同時另一個線程 B,按照先獲取鎖 b 再獲取鎖 a 的順序獲取鎖。以下圖所示: 其實線程 A 就是嶽不羣、線程 B 是林平之,葵花寶典上下冊分別是 lockA,lockBbash

死鎖

接着咱們用代碼模擬上線的執行過程,默認使用 SpringBoot 環境網絡

@Component
public class DeadLock {
    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public void deadLock() {
        Thread threadA = new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "獲取 lockA 成功");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + "嘗試獲取 lockB ");
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "獲取 lockB 成功");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        Thread threadB = new Thread(() -> {
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "獲取 lockB 成功 ");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + "嘗試獲取 lockA ");
                    synchronized (lockA) {
                        System.out.println(Thread.currentThread().getName() + "獲取 lockA 成功");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });

        threadA.start();
        threadB.start();
    }

}
複製代碼

單元測試併發

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
    @Autowired
    private DeadLock deadLock;

    @Test
    public void contextLoads() {
        deadLock.deadLock();
        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
複製代碼

控制檯打印工具

Thread-4獲取 lockB 成功 
Thread-3獲取 lockA 成功
Thread-3嘗試獲取 lockB 
Thread-4嘗試獲取 lockA 
複製代碼

咱們能夠發現 Thread-3 獲取 lockA 成功後嘗試獲取 lockB 一直不能成功。相互等待對方釋放造成了死鎖。單元測試

死鎖檢查

jstack 指令

該指令能夠生成虛擬機當前時刻的線程快照。線程快照是當前每一條線程正在執行的方法對戰的集合,主要目的是定位線程出現長時間停頓的緣由,好比 線程間死鎖死循環請求外部資源致使的長時間等待等。測試

先經過 jps 獲取正在執行的進程 id。ui

$ jps
23264 Jps
8472 JUnitStarter

複製代碼

再用jstack 查看當前進程的堆棧信息

$ jstack -F 8472
Attaching to process ID 8472, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13
Deadlock Detection:

Found one Java-level deadlock:
=============================

"Thread-4":
  waiting to lock Monitor@0x000000001f0134f8 (Object@0x00000007721d90f0, a java/lang/Object),
  which is held by "Thread-3"
"Thread-3":
  waiting to lock Monitor@0x000000001f011ef8 (Object@0x00000007721d90e0, a java/lang/Object),
  which is held by "Thread-4"

Found a total of 1 deadlock.

Thread 21: (state = BLOCKED)
 - com.zero.demo.deadlock.DeadLock.lambda$deadLock$1() @bci=79, line=35 (Interpreted frame)
 - com.zero.demo.deadlock.DeadLock$$Lambda$170.run() @bci=0 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)


Thread 20: (state = BLOCKED)
 - com.zero.demo.deadlock.DeadLock.lambda$deadLock$0() @bci=79, line=20 (Interpreted frame)
 - com.zero.demo.deadlock.DeadLock$$Lambda$169.run() @bci=0 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)

複製代碼

能夠看到存在死鎖 Found a total of 1 deadlock.

死鎖預防

咱們知道了死鎖如何產生的,那麼就知道該如何去預防。若是一個線程每次只能獲取一個鎖,那麼就不會出現因爲嵌套持有鎖順序致使的死鎖。

1. 正確的順序得到鎖

若是必須獲取多個鎖,咱們就要考慮不一樣線程獲取鎖的順序。

上面的例子出現死鎖的根本緣由就是獲取所的順序是亂序的,超乎咱們控制的。上面例子最理想的狀況就是把業務邏輯抽離出來,把獲取鎖的代碼放在一個公共的方法裏面,讓這兩個線程獲取鎖

都是從個人公共的方法裏面獲取,當Thread1線程進入公共方法時,獲取了A鎖,另外Thread2又進來了,可是A鎖已經被Thread1線程獲取了,因此只能阻塞等待。Thread1接着又獲取鎖B,Thread2線程就不能再獲取不到了鎖A,更別說再去獲取鎖B了,這樣就有必定的順序了。只有當線程1釋放了全部鎖,線程B才能獲取。

好比前面的例子咱們改爲

@Component
public class DeadLock {
    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public void deadLock() {
        Thread threadA = new Thread(() -> {
            getLock();
        });
        Thread threadB = new Thread(() -> {
            getLock();
        });

        threadA.start();
        threadB.start();
    }

    private void getLock() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "獲取 lockA 成功");
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "嘗試獲取 lockB ");
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "獲取 lockB 成功");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

}

複製代碼

查看打印結果,咱們發現 線程4 獲取成功而後線程3才能繼續獲取。

Thread-4獲取 lockA 成功
Thread-4嘗試獲取 lockB 
Thread-4獲取 lockB 成功
Thread-3獲取 lockA 成功
Thread-3嘗試獲取 lockB 
Thread-3獲取 lockB 成功
複製代碼

2. 超時放棄

當線程獲取鎖超時了則放棄,這樣就避免了出現死鎖獲取的狀況。當使用synchronized關鍵詞提供的內置鎖時,只要線程沒有得到鎖,那麼就會永遠等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,該方法能夠按照固定時長等待鎖,所以線程能夠在獲取鎖超時之後,主動釋放以前已經得到的全部的鎖。經過這種方式,也能夠頗有效地避免死鎖。

其餘死鎖

咱們再來回顧一下死鎖的定義,「死鎖是指兩個或兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。」 死鎖條件裏面的競爭資源,能夠是線程池裏的線程、網絡鏈接池的鏈接,數據庫中數據引擎提供的鎖,等等一切能夠被稱做競爭資源的東西。

線程池死鎖

final ExecutorService executorService = 
        Executors.newSingleThreadExecutor();
Future<Long> f1 = executorService.submit(new Callable<Long>() {

    public Long call() throws Exception {
        System.out.println("start f1");
        Thread.sleep(1000);//延時
        Future<Long> f2 = 
           executorService.submit(new Callable<Long>() {

            public Long call() throws Exception {
                System.out.println("start f2");
                return -1L;
            }
        });
        System.out.println("result" + f2.get());
        System.out.println("end f1");
        return -1L;
    }
});

複製代碼

線程池類型是單一線程,可是任務1依賴任務2的執行結果,因爲單線程模式,任務1沒有執行完,任務2永遠得不到執行,就死鎖了。

總結

在個人理解當中,死鎖就是「兩個任務以不合理的順序互相爭奪資源」形成,所以爲了規避死鎖,應用程序須要妥善處理資源獲取的順序。 另外有些時候,死鎖並不會立刻在應用程序中體現出來,在一般狀況下,都是應用在生產環境運行了一段時間後,纔開始慢慢顯現出來,在實際測試過程當中,因爲死鎖的隱蔽性,很難在測試過程當中及時發現死鎖的存在,並且在生產環境中,應用出現了死鎖,每每都是在應用情況最糟糕的時候——在高負載狀況下。所以,開發者在開發過程當中要謹慎分析每一個系統資源的使用狀況,合理規避死鎖,另一旦出現了死鎖,也能夠嘗試使用本文中提到的一些工具,仔細分析,老是能找到問題所在的。

關注公衆號 JavaStorm 轉發與點贊一塊兒牛逼。

相關文章
相關標籤/搜索