文章首发于:java并发编程(四)— 死锁的发生与避免
前言
上一篇我们介绍了如何通过synchronized 来加锁保护资源。但是,不当的加锁方式可能就会导致死锁。
死锁发生的场景
最典型的就是哲学家问题,
场景:5个哲学家,5跟筷子,5盘意大利面,大家围绕桌子而坐,进行思考与进食活动。
哲学家的活动描述:
哲学家除了吃面、还要思考、所以要么放下左右手筷子进行思考、要么拿起两个筷子(自己两侧的)开始吃面。
哲学家从不交谈,这就很危险了,很可能会发生死锁,假设每个人都是先拿到左边的筷子,然后去拿右边的筷子,那么就可能会出现如下情况。
通过代码模拟:
public class DeadLockTest2 { public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); int sum = 5; Chopsticks[] chopsticks = new Chopsticks[sum]; for (int i = 0; i < sum; i++) { chopsticks[i] = new Chopsticks(); } for (int i = 0; i < sum; i++) { executorService.execute(new Philosopher(chopsticks[i], chopsticks[(i + 1) % sum])); } } // 筷子 static class Chopsticks { } //哲学家 static class Philosopher implements Runnable { private Chopsticks left; private Chopsticks right; public Philosopher(Chopsticks left, Chopsticks right) { this.left = left; this.right = right; } @Override public void run() { try { //思考一段时间 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (left) { try { //拿到左边的筷子之后等待一段时间 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (right) { try { System.out.println("********开始吃饭"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
如上程序:定义了一个哲学家类,该类的主要任务要么是思考,要么是吃饭,吃饭的话,首先拿到其左边的筷子,等待一段时间后再去拿其右边的筷子。在此处因为每个哲学家都是占用自己左边的筷子等待拿右边的筷子。所以,就会出现循环等待,导致死锁。下面我们就来查看下:
如何查看死锁的发生
我们可以通过java命令很方便的查看是否有死锁发生。首先通过jps命令查看当前程序所占的进程如下:
找到对应的进程之后,接着通过jstack 命令查看程序运行情况。如下:
通过上述分析我们发现死锁发生的条件是如下四个(必须同时满足):
互斥,共享资源A和B只能被一个线程占用,就是本例中的,一根筷子同一时刻只能被一个哲学家获得
占有且等待:线程T1持有共享资源A,在等待共享资源B时,不释放占用的资源,在本例中就是:哲学家1获得他左边的筷子,等待获得他右边的筷子,即使没有得到也不会放回其获得的筷子。
不可抢占:其他线程不能强行占用线程T1占用的资源,在本例中就是:每个哲学家获得的筷子不能被其他哲学家抢走。
循环等待:线程T1等待线程T2占用的资源,线程T2等待线程T1占用的资源。在本例中:所有哲学家围坐一桌,已经形成了一个申请资源的环。
如何避免死锁
前面我们说了,死锁的发生条件是必须同时满足上述四个条件。那么避免死锁的方式就是破坏掉其中的一个条件就可以了。
对于占用且等待
对于占用且等待的情况,我们只需要一次性申请所有的资源,只有申请到了才会往下面走。对于这种情况,我们需要一个调度者,由它来统一申请资源。调度者必须是单例的,由他给哲学家分配筷子。
public class Allocator { private List