天天看點

面試官:手寫一個必然死鎖的例子?一頓操作猛如虎。。

前言

隻對死鎖代碼感興趣的可以直接跳到第三小節 必然死鎖示例,如果對死鎖還不太了解的,我們可以一起來讨論以下幾個議題

什麼是死鎖?

死鎖有什麼危害和特點?

代碼實作一個必然死鎖的示例

分析死鎖的過程

1.什麼是死鎖?

關鍵詞:并發場景,多線程

首先我們需要知道,死鎖一定發生在并發場景中。我們為了保證線程安全,有時會給程式使用各種能保證并發安全的工具,尤其是鎖,但是如果在使用過程中處理不得當,就有可能會導緻發生死鎖的情況。

關鍵詞:互不相讓

死鎖是一種狀态,當兩個(或多個)線程(或程序)互相持有對方所需要的資源,卻又都不主動釋放自己手中所持有的資源,導緻大家都擷取不到自己想要的資源,所有相關的線程(或程序)都無法繼續往下執行,在未改變這種狀态之前都不能向前推進,我們就把這種狀态稱為死鎖狀态,認為它們發生了死鎖。

簡而言之,死鎖就是兩個或多個線程(或程序)被無限期地阻塞,互相等待對方手中資源的一種狀态。

兩個線程死鎖的情況

面試官:手寫一個必然死鎖的例子?一頓操作猛如虎。。

如圖所示,線程1 已經持有了 鎖1,同時 線程2 也已經持有了鎖2,然後 線程1 嘗試擷取 鎖2,但是 線程2 并沒有釋放 鎖2,是以 線程1 處于阻塞狀态,同理可知,圖中的 線程2 擷取 鎖1也會被阻塞。

這樣一來,線程1 和 線程2 就發生了死鎖,因為它們都互相持有對方想要的資源,卻又不釋放自己手中的資源,形成互相等待,而且會一直等待下去。

2.死鎖的影響和危害

2.1 死鎖的影響

死鎖的影響在不同系統中是不一樣的,影響的大小一部分取決于目前這個系統或者環境對死鎖的處理能力。

2.1.1 資料庫中

例如,在資料庫系統軟體的設計中,考慮了監測死鎖以及從死鎖中恢複的情況。在執行一個事務的時候可能需要擷取多把鎖,并一直持有這些鎖直到事務完成。在某個事務中持有的鎖可能在其他事務中也需要,是以在兩個事務之間有可能發生死鎖的情況,一旦發生了死鎖,如果沒有外部幹涉,那麼兩個事務就會永遠的等待下去。

但資料庫系統不會放任這種情況發生,當資料庫檢測到這一組事務發生了死鎖時,根據政策的不同,可能會選擇放棄某一個事務,被放棄的事務就會釋放掉它所持有的鎖,進而使其他的事務繼續順利進行。

此時程式可以重新執行被強行終止的事務,而這個事務現在就可以順利執行了,因為所有跟它競争資源的事務都已經在剛才執行完畢,并且釋放資源了。

2.1.2 JVM 中

在 JVM 中,對于死鎖的處理能力就不如資料庫那麼強大了。如果在 JVM 中發生了死鎖,JVM 并不會自動進行處理,是以一旦死鎖發生,就會陷入無窮的等待。

2.2 死鎖的危害以及特點

關鍵詞:機率性事件

死鎖的問題和其他的并發安全問題一樣,是機率性的,也就是說,即使存在發生死鎖的可能性,也并不是 100% 會發生的。如果每個鎖的持有時間很短,那麼發生沖突的機率就很低,是以死鎖發生的機率也很低。但是線上上系統裡,可能每天有幾千萬次的“擷取鎖”、“釋放鎖”操作,在巨量的次數面前,整個系統發生問題的幾率就會被放大,隻要有某幾次操作是有風險的,就可能會導緻死鎖的發生。

也正是因為死鎖“不一定會發生”的特點,導緻提前找出死鎖成為了一個難題。壓力測試雖然可以檢測出一部分可能發生死鎖的情況,但是并不足以完全模拟真實、長期運作的場景,是以沒有辦法把所有潛在可能發生死鎖的代碼都找出來。

關鍵詞:危害大,發生幾率不高

一旦發生了死鎖,根據發生死鎖的線程的職責不同,就可能會造成 子系統崩潰、性能降低 甚至 整個系統崩潰 等各種不良後果。而且死鎖往往發生在高并發、高負載的情況下,因為可能會直接影響到很多使用者,造成一系列的問題。以上就是死鎖發生幾率不高但是危害大的特點。

3.必然死鎖示例

public class MustDeadLockDemo {

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        new Thread(new DeadLockTask(lock1, lock2, true), "線程1").start();
        new Thread(new DeadLockTask(lock1, lock2, false), "線程2").start();

    }

    static class DeadLockTask implements Runnable {

        private boolean flag;
        private Object lock1;
        private Object lock2;

        public DeadLockTask(Object lock1, Object lock2, boolean flag) {
            this.lock1 = lock1;
            this.lock2 = lock2;
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag) {
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + "->拿到鎖1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "->等待鎖2釋放...");
                    synchronized (lock2) {
                        System.out.println(Thread.currentThread().getName() + "->拿到鎖2");
                    }
                }
            }
            if (!flag) {
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + "->拿到鎖2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "->等待鎖1釋放...");
                    synchronized (lock1) {
                        System.out.println(Thread.currentThread().getName() + "->拿到鎖1");
                    }
                }
            }
        }
    }
}      

執行結果:

面試官:手寫一個必然死鎖的例子?一頓操作猛如虎。。

可以看到程式一直處于阻塞狀态。

4.過程分析

其實上面的代碼示例發生死鎖的過程就是第一小節中 兩個線程發生死鎖 的情況,這裡我們把圖拿過來,友善分析。

面試官:手寫一個必然死鎖的例子?一頓操作猛如虎。。

本文使用 IDEA 進行調試,将斷點打在 33 行,run方法的第一行,選擇 Thread 模式。

注意:調試過程,因為有人為的等待時間,是以并不會發生死鎖,這裡隻是示範線程執行的順序和狀态。

面試官:手寫一個必然死鎖的例子?一頓操作猛如虎。。

第一步,線程1進入,flag = true,進入第一個 synchronized 同步塊,拿到 lock1(鎖1)

面試官:手寫一個必然死鎖的例子?一頓操作猛如虎。。

第二步,直接點選 Resume Program(F9),進入線程2,此時 flag = false,進入第二個 synchronized 同步塊

面試官:手寫一個必然死鎖的例子?一頓操作猛如虎。。

當然如果 Thread.sleep 的時間夠長,或者操作速度夠快的話,也能發生死鎖。

5.總結

本章我們讨論了什麼是死鎖,以及死鎖的影響和危害,示範了一個必然死鎖的例子,然後使用 IDEA 工具調試了兩個線程發生死鎖的步驟。

在 JVM 中如果發生死鎖,可能會導緻程式部分甚至全部無法繼續向下執行的情況,是以死鎖在 JVM 中所帶來的危害和影響是比較大的,我們需要盡量避免。

最後如果在面試中碰到這一題,希望大家都能順利通過。