淺談Java中死鎖問題
1- Java中死鎖定義
在Java中synchronized關鍵字修飾的方法或者其他通過Lock加鎖方式修飾方法、代碼塊可以防止別的任務在還沒有釋放鎖的時候就訪問這個對象!如果一個任務在等待另一個任務持有的鎖,而後者又去等待其他任務持有的鎖,這樣一直下去,直到這個任務等待第一個任務持有的鎖,這樣就形成一個任務之間相互等待的連續循環,沒有哪個任務能夠繼續執行,此時所有任務停止,這種情況就是死鎖!
2- 死鎖實例
2.1-實例業務場景
本例來自Thinking in Java,具體的業務場景如下:
有5個哲學家去就餐,但是就只有5根筷子,這5個哲學家圍坐在一起,每個哲學家的左邊和右邊都有一根筷子。哲學家可能在思考,也可能在就餐。哲學家要就餐的話必須獲取到左邊和右邊兩根筷子,如果這個哲學家的左邊或者右邊的筷子已經正在被其他哲學家使用,則要就餐的哲學家必須等待其他哲學家就餐完畢釋放筷子!具體見下圖:
圖中圓圈表示哲學家,線條表示筷子!
2-2實現代碼
在本例中筷子屬於競爭資源,加鎖的方法肯定在筷子對應的類中,定義chopstick類,表示筷子,代碼如下:
package thread.test.deadLock;
/**
* 筷子類
*/
public class Chopstick {
//筷子的使用狀態,true-使用,false-未使用
private Boolean taken = false;
/**
* 使用筷子的方法
*
* @throws InterruptedException
*/
public synchronized void take() throws InterruptedException {
//如果筷子的狀態是使用,則等待
while (taken) {
wait();
}
//將筷子的狀態改成使用
taken = true;
}
/**
* 放下筷子方法
*/
public synchronized void drop() {
//將筷子的狀態改成未使用
taken = false;
//通知其他哲學家可以使用這根筷子
notifyAll();
}
}
在本例中哲學家屬於任務,哲學家類實現Runnable接口使用筷子即競爭資源,在run方法中哲學家先思考片刻,然後先拿右邊的筷子,後拿左邊的筷子,如果都拿到了那麼哲學家開始吃飯,然後先釋放右邊的筷子再釋放左邊的筷子!如果不能拿到左右兩根筷子,那麼哲學家持有一根筷子,等待另一根筷子被其他哲學家釋放!代碼如下:
package thread.test.deadLock;
import java.util.concurrent.TimeUnit;
/**
* 哲學家類
*/
public class Philosopher implements Runnable {
//哲學家左邊的筷子
private Chopstick left;
//哲學家右邊的筷子
private Chopstick right;
//哲學家編號
private final int id;
//哲學家思考
private final int ponderFactor;
public Philosopher(int id, int ponderFactor, Chopstick left, Chopstick right) {
this.id = id;
this.ponderFactor = ponderFactor;
this.left = left;
this.right = right;
}
/**
* 哲學家思考,思考時間ponderFactor
*
* @throws InterruptedException
*/
private void thinking() throws InterruptedException {
//如果不思考直接跳過
if (ponderFactor == 0) {
return;
}
//哲學家思考ponderFactor
TimeUnit.MILLISECONDS.sleep(ponderFactor);
}
/**
* run方法,提交的任務如果不打斷的話且沒有死鎖的話會一直進行下去
*/
@Override
public void run() {
try {
while (!Thread.interrupted()) {
//哲學家先思考片刻
System.out.println(this + " is thinking!");
thinking();
//拿右邊的筷子
System.out.println(this + " taking right chopstick!");
right.take();
System.out.println(this + " taked right chopstick!");
//拿左邊的筷子
System.out.println(this + " taking left chopstick!");
left.take();
System.out.println(this + " taked left chopstick!");
//吃飯
System.out.println(this + " is eating!");
//放下筷子
right.drop();
left.drop();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String toString() {
return "Philosopher" + id;
}
}
顯然,定義的Philosopher很容易造成死鎖,如果每個哲學家都是先拿右手邊的筷子或者先拿左手邊的筷子,那麼5個哲學家都是持有一個筷子並等待其他哲學家釋放筷子!此時,每個任務就會持有其他任務等待的鎖,形成一個相互等待的連續循環,從而造成死鎖!
下面演示下死鎖的示例代碼,代碼如下:
package thread.test.deadLock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 死鎖問題測試
*/
public class PhilosopherDeadLockTest {
public static void main(String... args) {
//創建線程池
ExecutorService executorService = Executors.newCachedThreadPool();
//創建5根筷子對象
Chopstick[] chopsticks = new Chopstick[5];
for (int i = 0; i < 5; i++) {
chopsticks[i] = new Chopstick();
}
//提交
for (int i = 0; i < 5; i++) {
executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5]));
}
executorService.shutdown();
}
}
運行下,打印結果如下:
由上圖可知,程序處於阻塞狀態,打印了5條Philosopher* taked right chopstick,表示這5個任務都先拿到了右邊的筷子,打印了5條Philosopher* taking left chopstick,大家都沒有拿到左邊的筷子,等處於等待的狀態!
由上圖可知,程序處於阻塞狀態,打印了5條Philosopher* taked right chopstick,表示這5個任務都先拿到了右邊的筷子,打印了5條Philosopher* taking left chopstick,大家都沒有拿到左邊的筷子,等處於等待的狀態!
3- 造成死鎖的原因
造成死鎖的原因有如下4條,且必須同時滿足:
1-互斥條件:任務使用的資源中至少有一個是不能共享的,本例中筷子都不能共享,需要競爭,筷子的使用和釋放方法都是用了synchronized關鍵字修飾!
2-至少有一個任務它必須持有一個資源且正在等待獲取一個當前正在被別的任務持有的資源,本例中哲學家必須持有一根筷子且其他筷子都被其他哲學家持有!
3-資源不能被任務搶佔,任務必須把資源釋放當做普通事件,資源只能被佔用的資源釋放後才能被其他任務獲取到!
4-必須有循環等待,這時一個任務等待其他任務釋放資源,其他任務又在等待另外一個任務釋放資源,且直到最後有一個任務在等待第一個任務釋放資源,使得大家都被鎖住!
4- 解鎖死鎖
知道了造成死鎖的原因了,那麼解決死鎖問題就是讓代碼邏輯不會同時滿足上述4條即可!
在本例中,可以讓前4個哲學家先獲取右邊的筷子然後獲取左邊的筷子,讓最後一個哲學家先獲取左邊的筷子然後獲取右邊的筷子,這樣第5個哲學家在獲取左邊的筷子的時候就會被阻塞,此時有一個筷子處於未被佔用的狀態,第1個哲學家就可以獲取到,吃完後就會釋放左右兩根筷子,從而不會造成死鎖問題!這是通過打破上述造成死鎖原因第4條的方式解決死鎖問題,具體代碼如下:
package thread.test.deadLock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 解決死鎖問題測試
*/
public class PhilosopherFixedDeadLockTest {
public static void main(String... args) {
//創建線程池
ExecutorService executorService = Executors.newCachedThreadPool();
//創建5根筷子對象
Chopstick[] chopsticks = new Chopstick[5];
for (int i = 0; i < 5; i++) {
chopsticks[i] = new Chopstick();
}
//提交
/**
* 解決死鎖:
* 前4個哲學家先拿右邊筷子,後拿左邊筷子
* 最後一個哲學家先拿左邊筷子,後拿右邊筷子
*/
for (int i = 0; i < 5; i++) {
if (i < 4) {
executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5]));
} else {
executorService.submit(new Philosopher(i, 5, chopsticks[0], chopsticks[5]));
}
}
executorService.shutdown();
}
}
運行發現可以很流暢的打印筷子的使用與釋放,不會有死鎖問題產生!
5- 總結
Java對死鎖沒有提供語言層面上的支持,只能通過程序員通過仔細的設計來避免死鎖!所以在設計多線程程序時,應該考慮造成死鎖的4個條件,絕對不能讓上述4個條件同時滿足!