摘录自 极客时间

死锁的出现

上一篇关于银行转账的文章(Java并发编程学习(三)之互斥锁)中使用了Account.class来使所有的转账串行化,但是实际操作中这么做简直性能不要太烂。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
```
实际上转账这个操作无疑问是并行操作,那么怎么样实现呢?可以通过两把锁来实现,一把锁住自己(`this`),一把锁住别人(`target`)。
```java
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
// 锁定转出账户
synchronized(this) {
// 锁定转入账户
synchronized(target) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
```
看上去这个细粒度锁没什么毛病,但是这个可能导致死锁。如果张三从A转账到B,李四从B转账到A,那么他们相互等待资源会直接死锁。

## 死锁的预防
在OS课上学过,要发生死锁一定要同时满足下面四个条件:
- 互斥。共享资源X和Y只能被一个线程占用。
- 占有且等待。拿了的资源不会释放。
- 不可抢占。其他线程不能拿走我的资源。
- 循环等待。线程A等待线程B占有的资源,线程B等待线程A占有的资源。

为了解决死锁问题,我们其实只要破坏上面这些条件随便一个就可以。

#### 破坏占用且等待
**一次性申请所有资源即可**。
创建一个第三方管理员,来统一处理资源的请求。如下代码:
```java
class Allocator {
private List<Object> als = new ArrayList<>();
// 一次性申请所有资源
synchronized boolean apply(Object from, Object to){
// 如果要申请的资源已经被谁占领了
if(als.contains(from) || als.contains(to)) {
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 归还资源
synchronized void free(Object from, Object to) {
als.remove(from);
als.remove(to);
}
}

class Account {
// actr 应该为单例
private Allocator actr;
private int balance;
// 转账
void transfer(Account target, int amt){
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))

try{
// 锁定转出账户
synchronized(this){
// 锁定转入账户
synchronized(target){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
actr.free(this, target)
}
}
}
```
#### 破坏不可抢占条件
核心是**主动释放它占有的资源**,但是`synchronized`做不到。申请不到资源的时候,线程会直接进入阻塞状态。
java.util.concurrect下的Lock可以解决这个问题。
#### 破坏循环等待
**对资源进行排序,按序申请**
①-⑥的代码对转出账户(`this`)和转入账户(`target`)进行了排序,然后按照序号从小到大顺序锁定账户。这样就破坏了循环等待条件。
```java
class Account {
private int id;
private int balance;
// 转账
void transfer(Account target, int amt){
Account left = this
Account right = target; ②
if (this.id > target.id) { ③
left = target; ④
right = this; ⑤
} ⑥
// 锁定序号小的账户
synchronized(left){
// 锁定序号大的账户
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}