一、死锁情景
有5个哲学家,围坐在一张圆桌子上,桌子上放着5根筷子,分别位于两个哲学家之间,如图所示:
他们有时会吃饭,有时会思考。吃饭时,哲学家会先拿起左手的那根筷子,再去拿右手的那根。当一个哲学家拿到两根筷子时,就可以开始吃饭,吃着吃着,又会放下筷子开始思考。
以上描述的场景,就会出现死锁:在某一时刻,所有的哲学家同时拿起左手边的筷子,再去拿右边的筷子时,发现已经被其他哲学家拿走了。此时,所有的哲学家都在等待另一根永远拿不到的筷子,就形成了死锁。
二、造成死锁的原因
造成死锁有3个条件,如果出现死锁,那么一定满足下面四个条件,但是,四个条件同时出现,不一定代表就是死锁。
- 资源互斥:一个资源只能被一个线程持有。也就是说,一根筷子只能被一个哲学家拿着,如果两个哲学家能同时使用一根筷子,那么就不会出现死锁;
- 资源不可抢夺:一个资源一旦被某个线程持有,除非这个线程自己放弃这个资源,否则没有人能抢走。套用在哲学家问题上,就是如果一个哲学家拿起了一根筷子,除非他主动将筷子放下,否则没有人能够将筷子从他的手上抢走;
- 循环等待:线程A持有了A资源,线程B持有了B资源,A在等待B释放B资源,B在等待A释放A资源。
三、使用Java模拟哲学家吃饭导致死锁问题
将问题简化为只有两个哲学家,java实现如下。
哲学家:
public class Philosopher implements Runnable {
private final String name;
private final Chopstick left;
private final Chopstick right;
public Philosopher(int id, Chopstick left, Chopstick right) {
this.name = "Philosopher-" + id;
this.left = left;
this.right = right;
}
@Override
public void run() {
for (;;) {
think();
eat();
}
}
private void eat() {
synchronized (left) {
System.out.println(name + " pick up left chopstick " + left);
left.pickUp();
synchronized (right) {
System.out.println(name + " pick up right chopstick " + right);
right.pickUp();
doEat();
System.out.println(name + " put down right chopstick " + right);
right.putDown();
}
System.out.println(name + " put down left chopstick " + left);
left.putDown();
}
}
private void doEat() {
System.out.println(name + " is eating.");
Tools.sleep(1000L);
System.out.println(name + " finish eating.");
}
private void think() {
System.out.println(name + " is thinking.");
Tools.sleep(1000L);
System.out.println(name + " finish thinking.");
}
public String getName() {
return name;
}
}
筷子:
class Chopstick {
private final int id;
private ChopstickState state;
public Chopstick(int id) {
this.id = id;
this.state = ChopstickState.PUT_DOWN;
}
public void pickUp() {
this.state = ChopstickState.PICK_PU;
}
public void putDown() {
this.state = ChopstickState.PUT_DOWN;
}
@Override
public String toString() {
return "Chopstick-" + id;
}
}
筷子状态:
enum ChopstickState {
PICK_PU,
PUT_DOWN,
;
}
一个简单的辅助类:
public class Tools {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序入口:
public class Main {
public static void main(String[] args) {
int philosopherNum = 2;
Chopstick[] chopsticks = createChopstick(philosopherNum);
Philosopher[] philosophers = createPhilosopher(chopsticks);
startPhilosopherThreads(philosophers);
}
private static Chopstick[] createChopstick(int num) {
Chopstick[] chopsticks = new Chopstick[num];
for (int i = 0; i < num; i++) {
chopsticks[i] = new Chopstick(i);
}
return chopsticks;
}
private static Philosopher[] createPhilosopher(Chopstick[] chopsticks) {
int size = chopsticks.length;
Philosopher[] philosophers = new Philosopher[size];
for (int i = 0; i < size; i++) {
int left = i;
int right = i + 1;
right = right == size ? 0 : right;
philosophers[i] = new Philosopher(i, chopsticks[left], chopsticks[right]);
}
return philosophers;
}
private static void startPhilosopherThreads(Philosopher[] philosophers) {
for (Philosopher philosopher: philosophers) {
Thread thread = new Thread(philosopher, philosopher.getName());
thread.start();
}
}
}
启动程序后,我们使用VisualVM查看程序的ThreadDump,部分如下:
"Philosopher-1" #14 prio=5 os_prio=0 tid=0x000001ef72af9800 nid=0xe20 waiting for monitor entry [0x000000cae1dff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at philosopher.Philosopher.eat(Philosopher.java:27)
- waiting to lock <0x0000000088b19428> (a philosopher.Chopstick)
- locked <0x0000000088b19440> (a philosopher.Chopstick)
at philosopher.Philosopher.run(Philosopher.java:18)
at java.lang.Thread.run(java.base@9/Thread.java:844)
Locked ownable synchronizers:
- None
"Philosopher-0" #13 prio=5 os_prio=0 tid=0x000001ef72af8800 nid=0x3074 waiting for monitor entry [0x000000cae1cff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at philosopher.Philosopher.eat(Philosopher.java:27)
- waiting to lock <0x0000000088b19440> (a philosopher.Chopstick)
- locked <0x0000000088b19428> (a philosopher.Chopstick)
at philosopher.Philosopher.run(Philosopher.java:18)
at java.lang.Thread.run(java.base@9/Thread.java:844)
Locked ownable synchronizers:
- None
我们可以看出,线程Philosopher-1
持有0x0000000088b19440
,等待0x0000000088b19428
。线程Philosopher-0
持有0x0000000088b19428
,等待0x0000000088b19440
,因而形成死锁。
四、如何解决死锁
解决死锁,只需要将消除形成它的三个必要条件的其中一个即可。我们这里使用一个比较简单的方案:解决循环等待。一个哲学家最多只会等待3秒钟,如果无法得到想要的筷子,那么他就会放下手中的筷子(如果有)。Java实现如下:
哲学家
public class Philosopher implements Runnable {
private final String name;
private final Chopstick left;
private final Chopstick right;
public Philosopher(int id, Chopstick left, Chopstick right) {
this.name = "Philosopher-" + id;
this.left = left;
this.right = right;
}
@Override
public void run() {
for (; ; ) {
think();
try {
eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void eat() throws InterruptedException {
Random random = new Random();
if (left.getLock().tryLock(random.nextInt(3), TimeUnit.SECONDS)) {
try {
System.out.println(name + " pick up left chopstick " + left);
left.pickUp();
if (right.getLock().tryLock(random.nextInt(3), TimeUnit.SECONDS)) {
try {
System.out.println(name + " pick up right chopstick " + right);
right.pickUp();
doEat();
} finally {
System.out.println(name + " put down right chopstick " + right);
right.putDown();
right.getLock().unlock();
}
}
} finally {
System.out.println(name + " put down left chopstick " + left);
left.putDown();
left.getLock().unlock();
}
}
}
private void doEat() {
System.out.println(name + " is eating.");
Tools.sleep(1000L);
System.out.println(name + " finish eating.");
}
private void think() {
System.out.println(name + " is thinking.");
Tools.sleep(1000L);
System.out.println(name + " finish thinking.");
}
public String getName() {
return name;
}
}
筷子
class Chopstick {
private final int id;
private ChopstickState state;
private final ReentrantLock lock = new ReentrantLock();
public Chopstick(int id) {
this.id = id;
this.state = ChopstickState.PUT_DOWN;
}
public void pickUp() {
this.state = ChopstickState.PICK_PU;
}
public void putDown() {
this.state = ChopstickState.PUT_DOWN;
}
public Lock getLock() {
return lock;
}
@Override
public String toString() {
return "Chopstick-" + id;
}
}
其他代码与原有代码相同。