1. 前言
本文會從一個小例子開始,介紹什麼是死鎖,再針對例子,說明如何避免死鎖,最後會介紹一些死鎖的理論化知識。
2. 正文
2.1 死鎖的小例子
class TaskA implements Runnable {
@Override
public void run() {
while (true) {
synchronized (DeadLockDemo.lock1) {
System.out.println(Thread.currentThread().getName() + " holds lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waits for lock2");
synchronized (DeadLockDemo.lock2) {
System.out.println(Thread.currentThread().getName() + " hold2 lock2");
}
}
}
}
}
class TaskB implements Runnable {
@Override
public void run() {
while (true) {
synchronized (DeadLockDemo.lock2) {
System.out.println(Thread.currentThread().getName() + " holds lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waits for lock1");
synchronized (DeadLockDemo.lock1) {
System.out.println(Thread.currentThread().getName() + " holds lock1");
}
}
}
}
}
public class DeadLockDemo {
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(new TaskA(), "ThreadA");
Thread threadB = new Thread(new TaskB(), "ThreadB");
threadA.start();
threadB.start();
}
}
運行這個小例子,會有四種打印結果:
第一種:
ThreadA holds lock1
ThreadB holds lock2
ThreadB waits for lock1
ThreadA waits for lock2
第二種:
ThreadA holds lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadB waits for lock1
第三種:
ThreadB holds lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadA waits for lock2
第四種:
ThreadB holds lock2
ThreadA holds lock1
ThreadA waits for lock2
ThreadB waits for lock1
我們只按第一種打印來分析這個小例子。
ThreadA holds lock1
ThreadB holds lock2
ThreadB waits for lock1
ThreadA waits for lock2
我們先看一下程序代碼,再去分析打印結果。
TaskA
和 TaskB
分別是兩個任務:在 TaskA
的 run()
方法中,先獲取鎖 lock1
,休眠 100 ms 後,再去獲取鎖 lock2
;在 TaskB
的 run()
方法中,先獲取鎖 lock2
,休眠 100 ms 後,再去獲取鎖 lock1
。
在 main()
方法中,開啓 ThreadA
和 ThreadB
兩個線程,分別執行 TaskA
和 TaskB
這兩個任務。
從打印結果可以看到,
ThreadA
先獲取了鎖 lock1
,接着 ThreadB
獲取了鎖 lock2
;
ThreadB
打算獲取鎖 lock1
,但是 lock1
被 ThreadA
持有着不釋放,所以 ThreadB
此時因無法獲得鎖 lock1
而處於阻塞狀態;
ThreadA
打算獲取鎖 lock2
,但是 lock2
被 ThreadB
持有着不釋放,所以 ThreadA
此時因無法獲得鎖 lock2
而處於阻塞狀態。
到這裏,ThreadB
和 ThreadA
都處於阻塞狀態,因爲它們爲了獲取彼此持有的鎖而不得。這種情況就造成了死鎖。
2.2 數據庫的死鎖
一個數據庫事務可能由多條SQL更新請求組成。當在一個事務中更新一條記錄,這條記錄就會被鎖住避免其他事務的更新請求,直到第一個事務結束。同一個事務中每一個更新請求都可能會鎖住一些記錄。
當多個事務同時需要對一些相同的記錄做更新操作時,就很可能發生死鎖。
Transaction 1, request 1, locks record 1 for update
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.
因爲鎖發生在不同的請求中,並且對於一個事務來說不可能提前知道所有它需要的鎖,因此很難檢測和避免數據庫事務中的死鎖。
2.3 對死鎖的描述
死鎖是兩個或更多線程互相持有對方所需要的資源,導致這些線程處於阻塞狀態,無法執行的情況。
死鎖產生的 4 個必要條件:
互斥條件:進程使用所分配到的資源時具有排他性,也就是說,在一段時間內只能由獲取資源的進程佔用該資源;如果在這段時間內,有其他進程請求資源,則請求方只能等待佔有資源的進程釋放資源。
請求和保持條件:進程已經保持至少一個資源,但又提出了新的資源請求,而新的資源已被其他進程佔有,此時請求進程阻塞;但請求進程又對自己已獲得的資源保持不釋放。
不剝奪條件:進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完畢後由進程自己釋放。
環路等待條件:在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合 {P0, P1,……,Pn} 中的 P0 正在等待 P1 佔用的資源,P1 正在等待 P2 佔用的資源,……,Pn正在等待已被 P0 佔用的資源。這個條件也要求在多操作者(M>=2個)情況下,爭奪多個資源(N>=2個,且N<=M)。
在 2.1 中的死鎖小例子滿足這 4 個必要條件:
ThreadA
獲得了鎖 lock1
,在沒有釋放之前 ThreadB
不能使用,所以滿足互斥條件。
ThreadA
已經獲得了鎖 lock1
,又去請求獲取鎖 lock2
,而鎖 lock2
被 ThreadB
所持有, ThreadA
就處於阻塞狀態,這時 ThreadA
在請求鎖 lock2
,而 ThreadB
又保持鎖 lock2
不放,所以滿足請求和保持條件。
ThreadB
獲取了鎖 lock2
,只能等 ThreadB
自行釋放,而不會被剝奪,所以滿足不可剝奪條件。
ThreadA
在等待獲取鎖 lock2
,ThreadB
在等待獲取鎖 lock1
,這符合環路等待條件。
2.4 避免死鎖
這部分會採取一些辦法,針對 2.1 中的死鎖小例子,避免死鎖。
死鎖的 4 個必要條件,只要有一個不滿足,就可以避免死鎖。
互斥條件,不可剝奪條件,這兩個不可以打破,因爲這是保證線程同步的條件。
打破保持和請求條件
首先,把 2.1 中的小例子改成使用顯式鎖 ReentrantLock
的形式,這種形式的效果和 synchronized
內置鎖是一樣的。
class TaskA implements Runnable {
@Override
public void run() {
while (true) {
DeadLockDemo.lock1.lock();
try {
System.out.println(Thread.currentThread().getName() + " holds lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waits for lock2");
DeadLockDemo.lock2.lock();
try {
System.out.println(Thread.currentThread().getName() + " hold2 lock2");
} finally {
DeadLockDemo.lock2.unlock();
}
} finally {
DeadLockDemo.lock1.unlock();
}
}
}
}
class TaskB implements Runnable {
@Override
public void run() {
while (true) {
DeadLockDemo.lock2.lock();
try {
System.out.println(Thread.currentThread().getName() + " holds lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waits for lock1");
DeadLockDemo.lock1.lock();
try {
System.out.println(Thread.currentThread().getName() + " holds lock1");
} finally {
DeadLockDemo.lock2.unlock();
}
} finally {
DeadLockDemo.lock2.unlock();
}
}
}
}
public class DeadLockDemo {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread threadA = new Thread(new TaskA(), "ThreadA");
Thread threadB = new Thread(new TaskB(), "ThreadB");
threadA.start();
threadB.start();
}
}
其次,使用 ReentrantLock
的 tryLock()
方法:僅在調用時鎖未被另一個線程保持的情況下,才獲取該鎖。
比如現在 ThreadA
獲取了鎖 lock1
,ThreadB
獲取了鎖 lock2
,而 ThreadB
調用 lock1.tryLock()
嘗試獲取鎖 lock1
,此時鎖 lock1
被 ThreadA
保持,lock1.tryLock()
返回 false
,ThreadB
就繼續執行代碼,釋放自己持有的鎖 lock2
。而不是像之前那樣,因沒有獲取鎖 lock1
,而處於阻塞狀態。
下面是代碼實現:
class TaskA implements Runnable {
@Override
public void run() {
while (true) {
if (DeadLockDemo.lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " holds lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waits for lock2");
if (DeadLockDemo.lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " hold2 lock2");
} finally {
DeadLockDemo.lock2.unlock();
}
}
} finally {
DeadLockDemo.lock1.unlock();
}
}
}
}
}
class TaskB implements Runnable {
@Override
public void run() {
while (true) {
if (DeadLockDemo.lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " holds lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waits for lock1");
if (DeadLockDemo.lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " holds lock1");
} finally {
DeadLockDemo.lock1.unlock();
}
}
} finally {
DeadLockDemo.lock2.unlock();
}
}
}
}
}
運行程序,查看日誌:
ThreadA holds lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadB holds lock2
不會發生死鎖,日誌會一直打印。但是,大家如果在日誌中搜索 ThreadA holds lock2
,卻找不到。這說明 ThreadA
沒有獲取到 lock2
。
這是爲什麼呢?
ThreadA
和 ThreadB
兩個線程在嘗試拿鎖的過程中,發生線程之間互相謙讓,不斷髮生同一個線程總是拿到同一把鎖,在嘗試拿另一把鎖時因爲拿不到,而將本來已經持有的鎖釋放,這就造成了活鎖。
解決辦法:每個線程休眠隨機數,錯誤拿鎖的時間。
在兩個任務的 while
循環的最後一行添加代碼:
try {
Thread.sleep(new Random().nextInt(3));
} catch (InterruptedException e) {
e.printStackTrace();
}
運行一下,看日誌:
ThreadB holds lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadB holds lock2
ThreadB waits for lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadB holds lock2
打破環路等待條件
保證所有的線程都按相同的順序獲取鎖,避免死鎖發生。
針對 2.1 的死鎖小例子,修改 TaskB
:
class TaskB implements Runnable {
@Override
public void run() {
while (true) {
synchronized (DeadLockDemo.lock1) {
System.out.println(Thread.currentThread().getName() + " holds lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waits for lock2");
synchronized (DeadLockDemo.lock2) {
System.out.println(Thread.currentThread().getName() + " holds lock2");
}
}
}
}
}
這是爲了使 ThreadB
和 ThreadA
一樣都按照先獲取鎖 lock1
,再獲取鎖 lock2
的順序來獲取鎖。
打印結果:
ThreadA holds lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1