1 什麼是死鎖
死鎖是多個進程\線程爲了完成任務申請多個不可剝奪的資源並且以不正確的方式推進導致的一直互相等待對方釋放資源的狀態。下面以經典的哲學家就餐問題爲例,描述死鎖產生的場景。
2 哲學家就餐問題
五個哲學家坐在一個圓桌上,每個哲學家兩側都放着1根筷子,總共有5只筷子。哲學家需要分別或者左右手的兩隻筷子才能就餐,就餐完成後將筷子放回原處,其他哲學家可以獲取放回的筷子。有這樣一種狀態,每個哲學家都獲取了他右手的筷子,試圖獲取左手的筷子時都會失敗(被他左手邊的哲學家拿走了),然後所有哲學家都會一直等待他左手邊哲學家釋放筷子,這就導致了死鎖狀態。
public class PhilosopherEat {
/*
*筷子類
*/
public static class Chop {
private volatile boolean taken = false; //筷子狀態
ReentrantLock lock = new ReentrantLock(); //定義鎖
Condition isTaken = lock.newCondition();
//拿起筷子
public void take() throws InterruptedException {
lock.lock();
try {
while (taken) { //筷子已被其他哲學家拿走
isTaken.await();
}
taken = true; //標記筷子被拿走
} finally {
lock.unlock();
}
}
// 放下筷子
public void put() throws InterruptedException {
lock.lock();
try {
taken = false; //放下筷子
isTaken.signalAll(); //通知鄰座的哲學家拿筷子
} finally {
lock.unlock();
}
}
}
/*
* 哲學家就餐類
*/
public static class Philosopher implements Runnable {
private Chop left; //左手的筷子
private Chop right; //右手的筷子
private int id; //哲學家編號
private int ponderFactor; //思考時間
private Random random = new Random(47);
//暫停時間,模擬哲學家吃飯用時等
private void pasue() throws InterruptedException {
if (ponderFactor == 0) {
return;
}
//TimeUnit.MILLISECONDS.sleep(random.nextInt(ponderFactor * 250));
TimeUnit.MILLISECONDS.sleep(10);
}
//構造方法
public Philosopher(Chop left, Chop right, int id, int ponderFactor) {
this.left = left;
this.right = right;
this.id = id;
this.ponderFactor = ponderFactor;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println(this + " " + "thinking");
pasue();
right.take();
System.out.println(this + " " + "take right");
left.take();
System.out.println(this + " " + "take left");
System.out.println(this + " " + "eat");
pasue();
left.put();
System.out.println(this + " " + "put left");
right.put();
System.out.println(this + " " + "put right");
}
} catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
int size = 5;
int ponder = 5;
Chop [] chops = new Chop[5]; //5跟筷子
for (int i = 0; i < 5; i++) {
chops[i] = new Chop();
}
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < size; i++) {
pool.execute(new Philosopher(chops[i], chops[(i + 1) % 5], i, ponder));
}
try {
System.out.println("quit");
System.in.read();
} catch (IOException e) {}
pool.shutdown();
}
}
大部分情況下執行不會發生死鎖,就餐和思考時間越短越容易發生死鎖,這也是死鎖問題的可怕之處,不易復現。
3 死鎖的必要條件
死鎖的必要條件有如下四個:
3.1 互斥條件
一個資源每次只能被一個線程使用,如IO等。
3.2 請求與保持條件
一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
3.3 不剝奪條件
進程已獲得的資源,在未使用完之前,不能強行剝奪。
3.4 循環等待條件
若干進程之間形成一種頭尾相接的循環等待資源關係。
4 解決死鎖的方法
死鎖的必要條件必須全部滿足纔會產生死鎖,所以要解決死鎖問題只需要任意破壞其中一個條件就可以解決死鎖問題。
4.1 互斥條件
很多系統資源如IO等必須是互斥的,破壞互斥條件的成本較大。
4.2 請求與保持條件
可以通過一次性獲取所有資源即對需要的資源進行原子申請可以解決死鎖問題,這種方式對系統開銷較大,不太理想。
4.3 不可剝奪條件
可以通過定時釋放佔有的資源解決死鎖問題,但是這也會帶來過多的資源佔有釋放操作。
4.4 循環等待條件
這是解決死鎖常用的方法,例如哲學家就餐問題中,最後一個哲學家可以先拿左手的筷子,拿不到就會等待,他右手的筷子就可以供第一個哲學家使用。
public static void main(String[] args) {
int size = 5;
int ponder = 5;
Chop [] chops = new Chop[5]; //5跟筷子
for (int i = 0; i < 5; i++) {
chops[i] = new Chop();
}
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < size; i++) {
if (i < size - 1) {
pool.execute(new Philosopher(chops[i], chops[(i + 1) % 5], i, ponder));
} else {
pool.execute(new Philosopher(chops[0], chops[i], i, ponder));
}
}
try {
System.out.println("quit");
System.in.read();
} catch (IOException e) {}
pool.shutdown();
}