1.死锁的情况
针对有关联的资源,我们用锁进行保护的同时,在去求锁的粒度和性能之间,很容易就产生死锁问题,例如针对互斥锁章节的问题。我们可以进行如下处理:

代码如下:
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;
}
}
}
}
}
此时如果我们同时调用Account A的transfer(B,100)和Account B的transf(A, 100)就会发生死锁问题,其实就是ABBA问题,AB之间互相持有锁并相互等待的场景。
死锁的一个比较专业的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
2.如何预防死锁
死锁只有以下四个条件同时发送时,才会出现死锁:
- 互斥:共享资源X和Y只能被一个线程占有
- 占用且等待:线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
- 不可抢占:其他线程不可抢占线程T1占用的资源
- 循环等待:线程T1等待线程T2占用的资源,线程T2等待线程T1占用的资源,就是循环等待
也就是说只需要破坏上述某一个条件,就可以成功避免死锁:
针对条件1互斥,由于用锁就是为了互斥,这里无法破坏,因此针对234点入手:
- 对于“占用且等待”:一次性申请所有的资源,就能避免
- 对于“不可抢占”:占用部分资源的线程进一步申请其他资源时,如果申请不到,就可以释放当前资源
- 对于“循环-等待”:可以靠有序申请资源来预防。所谓按序申请,是指资源是有线性顺序的
2.1.破坏“占用且等待”
理论上:破坏这个条件,可以一次性申请所有资源。对应到编程领域,一次性申请也可以看做一个临界区,并且针对该临界区采取互斥机制,来保证申请所有资源的原子性。
2.2.破坏“不可抢占条件”
核心是要能够主要释放抢占的资源,这点synchronized无法做到。java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的
2.3.破坏“循环等待条件”
破坏这个条件,需要对资源进行排序,然后按序申请资源。例如该转账的场景,就可以按照账户某个属性进行排序,按序申请资源。
代码如下:
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;
}
}
}
}
}
3.用“等待-通知”机制优化“循环-等待”
我们上面使用while循环来实现循环等待机制,但是如果循环内部的操作比较耗时则会比较影响性能,我们是否还有更优的方式实现循环等待呢,我们可以再线程要求的条件无法得到满足,则阻塞进入等待状态,当阻塞线程要求的条件满足时,通知等待的线程再去请求锁,那么我们使用while死循环的方式就能取消,从而避免过度消耗CPU
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
;
当线程T不能够一次性拿到所以资源时,为了避免死锁,则可以阻塞并等待所有资源可以申请,此时就引入了“等待-通知”机制。
完整的等待-通知机制:线程首先请求锁,请求成功后当执行完毕或当前线程申请的锁资源不满足,则释放锁,进入等待状态;当要求满足时,通知等待的线程,再次去请求锁。(zookeeper实现的分布式锁有点类似于该机制)
3.1.用 synchronized 实现等待 - 通知机制
Java中有多重方式实现等待-通知机制,例如Java 语言内置的 synchronized 配合 wait()、notify()、notifyAll() 这三个方法就能轻松实现
等待队列:等待队列和互斥锁是一对一的关系,每个互斥锁都有自己的等待队列,当有一个线程进入临界区时,其余线程就会进入等待队列。

wait():在并发程序中,有些程序进入了临界区之后,由于某些条件不满足,需要进入等待状态。此时可以调用wait()方法,调用之后当前线程就会被阻塞,并且进入到右边的等待队列中,并且会释放该线程持有的锁。
notify()/ notyfyAll():当条件满足时调用notify(),会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过(你所需要的条件都满足了,可以再去请求锁啦),并且被通知的线程会去重新获得锁,并且需要注意的是wait()、notify()、notifyAll() 方法操作的等待队列是互斥锁的等待队列,如果 synchronized 锁定的是 this,那么对应的一定是 this.wait()、this.notify()、this.notifyAll();如果 synchronized 锁定的是 target,那么对应的一定是 target.wait()、target.notify()、target.notifyAll() 。而且 wait()、notify()、notifyAll() 这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()、notify()、notifyAll() 都是在 synchronized{}内部被调用的。如果在 synchronized{}外部调用,或者锁定的 this,而用 target.wait() 调用的话,JVM 会抛出一个运行时异常:
java.lang.IllegalMonitorStateException
曾经满足过?:因为notify()只保证在通知时间点条件是满足的,因此当队列被通知的线程执行时,条件可能又不满足了。

3.2.等待-通知机制的范式
- 互斥锁:确认互斥锁是什么,转账的场景中的锁是Allocator的实例this
- 线程要求的条件是什么:锁资源同时能够满足
- 何时等待:线程要求的条件不满足则等待,如无法同时申请锁资源
- 何时通知:阻塞线程要求的条件满足则通知,如能够同时申请锁资源这一条件
3.3.尽量使用notifyAll()
notify()是会随机的通知等待队列的一个线程,而notifyAll()会通知等待队列中的所有线程。所以除非经过深思熟虑,否则尽量使用 notifyAll()。因为notify()可能会导致某些线程永远不会被通知到或者通知到了也无法继续执行