死锁是常见的线程活性问题。多个线程互相等待对方而导致永远暂停称为死锁。常见的死锁q情况是线程A持有锁L1的情况下申请锁L2,线程B持有锁L2申请锁L1。线程A等待锁L2释放,线程B等待锁L1释放,从而互相等待永远暂停。
死锁的必要条件
- 资源互斥,资源同时只能被一个线程占用。
- 资源不可抢夺,资源被一个线程占用时,其他线程无法抢夺。
- 占用并等待资源,线程持有资源,并申请另外的资源而进入等待时。不会释放现有资源。
- 循环等待资源,线程A持有锁L1的情况下申请锁L2,线程B持有锁L2申请锁L1。线程A等待锁L2释放,线程B等待锁L1释放。A持有L1等L2,B持有L2等L1 这就是循环等待。
哲学家用餐问题
哲学家用餐问题是经典的死锁问题。一群哲学家围着一个大圆桌坐下,每个哲学家面前都有一个碗和一根筷子。哲学家要么思考要么吃饭。哲学家吃饭时总是先拿起左手边的筷子再拿起右手边的筷子。只有拿到2根筷子的哲学家可以吃饭。哲学家吃饭吃着会放下筷子,再次思考。
将问题简化,假设只有2个哲学家。哲学家p1想吃饭,先拿起他左边的筷子c1。这时哲学家p2也想吃饭,拿起他左边的筷子c2。哲学家相当于线程,筷子相当于锁。这样就进入死锁状态。
代码模拟
哲学家抽象类
定义哲学家的吃饭,思考动作和 身份标识id,左右筷子属性。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
42public abstract class AbstractPhilosopher extends Thread {
protected int id;
protected Chopsticks left;
protected Chopsticks right;
public AbstractPhilosopher(int id,Chopsticks left,Chopsticks right){
this.id=id;
this.left=left;
this.right=right;
}
public void run() {
//哲学家不是在思考,就是在吃饭
while (true){
think();
eat();
}
}
public abstract void eat();
public void think(){
try {
System.out.println(String.format("%s thinking",this.toString()));
Thread.sleep(new Random().nextInt(2)*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void doEat(){
try {
System.out.println(String.format("%s eating",this.toString()));
Thread.sleep(new Random().nextInt(2)*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String toString() {
return String.format("Philosopher%d",this.id);
}
}
筷子类
筷子类充当锁对象,获取锁相当于拿起筷子,释放锁相当于放下筷子。1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Chopsticks {
private int id;
public Chopsticks(int id){
this.id=id;
}
public int getId() {
return id;
}
public String toString() {
return String.format("Chopsticks%d",id);
}
}
哲学家实现类
1 | public class DeadLockPhilosopher extends AbstractPhilosopher { |
模拟
1 | public static void main(String[] args) throws InterruptedException { |
运行结果1
2
3
4Philosopher1 thinking
Philosopher2 thinking
Philosopher2拿起Chopsticks2
Philosopher1拿起Chopsticks1
哲学家p2拿起了筷子c2,哲学家p1拿起了筷子c1。产生死锁。
避免死锁的解决方法
死锁的解决方法可以从死锁的产生条件入手。只要消除死锁四个必要条件之一就可以避免死锁。
粗粒锁法
通过一个大的锁来替代多个细粒度的锁。由于只有一个锁,死锁的必要条件占用并等待资源和循环等待资源都不成立。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class BigLogPhilosopher extends AbstractPhilosopher {
private final static Object GLOBAL_LOCK=new Object();
public BigLogPhilosopher(int id, Chopsticks left, Chopsticks right) {
super(id, left, right);
}
public void eat() {
synchronized (GLOBAL_LOCK){
//拿起左边
System.out.println(String.format("%s拿起%s",this.toString(),this.left.toString()));
//拿起右边筷子
System.out.println(String.format("%s拿起%s",this.toString(),this.right.toString()));
this.doEat();
}
}
}
粗粒锁的缺点是降低了并发,浪费资源。如果有4个哲学家的情况下,一个哲学家吃饭,还剩下2根筷子可以给一个哲学家同时用餐。
但是用粗粒锁的方法同时只能一个哲学家吃饭。
锁排序法
锁排序是给锁进行排序,所有线程申请锁都按排好的顺序。从而消除循环等待条件。
如哲学家问题中,给筷子排序,每次哲学家不按照左右顺序来拿筷子。而是都拿id小的筷子再拿大的。
所以哲学家p1,p2都会先拿筷子c1。当哲学家p1拿到筷子c1后,哲学家p2获取不到锁进入阻塞。死锁不会发送。1
2
3
4
5
6
7
8
9
10public class SortLockPhilosopher extends DeadLockPhilosopher {
public SortLockPhilosopher(int id, Chopsticks left, Chopsticks right) {
super(id, left, right);
if(left.getId()>right.getId()){
this.left=right;
this.right=left;
}
}
}
1 | public static void main(String[] args) throws InterruptedException { |
用tryLock(long,TimeUnit)
第三种方法是通过tryLock(long,TimeUnit),固定时间内获取不了锁就获取失败返回false。哲学家问题中,可以在超时获取不到锁的情况下,将本来就持有的锁放下,即可消除占有并等待资源条件,避免死锁。