前言
隻對死鎖代碼感興趣的可以直接跳到第三小節 必然死鎖示例,如果對死鎖還不太了解的,我們可以一起來讨論以下幾個議題
什麼是死鎖?
死鎖有什麼危害和特點?
代碼實作一個必然死鎖的示例
分析死鎖的過程
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 中所帶來的危害和影響是比較大的,我們需要盡量避免。
最後如果在面試中碰到這一題,希望大家都能順利通過。