《java并发编程实战笔记》
第十章 活跃性危险与如何避免
活跃性危险在第一章介绍,包括死锁、饥饿、活锁。
死锁
经典的“哲学家进餐”问题: 5个哲学家去吃中餐,坐在一张圆桌旁,他们有五根筷子(不是五双),并且每两个人中间放一根筷子。哲学家们时而思考,时而进餐。每个人都需要一双筷子才能吃到东西,并且在吃完后将筷子放回原处继续思考。 如果每个人都立即抓住自己左边的筷子,然后等待自己右边的筷子空出来,但同时又不放下已经拿到的筷子。——产生死锁
也就是 每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得所有需要的资源之前都不会放弃已经拥有的资源。 (正确的做法是尝试获得两根邻近的筷子,如果其中一根正在被另一个哲学家使用,那么他应该放弃自己得到的那根,等待一段时间后再尝试。)
当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。 在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去。这种情况就是最简单的死锁形式(或者称为“抱死[Deadly Embrace]“) 。
在数据库系统的设计中考虑了监测死锁以及从死锁中恢复,在执行一个事务时可能需要获得多个锁,并一直持有这些锁直到事务提交。因此两个事务之间很可能发生死锁,但事实上这种情况并不多见。数据库服务器不会让这种情况发生,当它检测到一组事务发生死锁时,将选择一个牺牲者并放弃这个事务。作为牺牲者的事务会释放它所持有的资源,从而使其他事务继续进行。
而java解决死锁问题方面没有数据库服务那么强大,发生死锁时可能造成应用程序完全停止,或者某个特定的子系统停止,或者性能降低,恢复应用程序唯一方式就是中止并重启。解决死锁的思路,所有线程以固定的顺序来获得锁,在程序中就不会出现锁顺序死锁问题。
顺序死锁
原因:发生死锁的原因是两个线程A和B试图以不同的顺序来获得相同的锁。如果按照相同的顺序来请求锁,就不会出现循环的加锁依赖性,因此也就不会发生死锁
解决方法:按照相同的顺序来请求锁
//容易发生死锁
public class LeftRightDeadlock {
private final Object left = new Object() ;
private final Object right = new Object() ;
public void leftRight(){
synchronized (left) {
synchronized (right) {
//doSomething();
}
}
}
public void rightLeft(){
synchronized (right) {
synchronized (left) {
//doSomethingElse();
}
}
}
}
动态的顺序死锁
动态的顺序死锁不同于LeftRightDeadlock 一眼就能看出,很多看似无害的代码,当运行后才能发现出现死锁。动态的顺序死锁原因在于并不清楚是否在锁顺序上有足够的控制权来避免死锁的发生。
如果两个线程同时调用transferMoney,其中一个线程从X往Y转账,另一个线程从Y往X转账。很有可能A获得myAccount的锁等待yourAccount的锁,然而B此时持有yourAccount锁等待myAccount的锁。
transferMoney(myAccount,yourAccount,10);
transferMoney(yourAccount,myAccount,20);
public class DynamicDeadlock {
/**
* 转账操作(会发生死锁)
* 两个账户相互给对方转账时,会发生死锁,相当于简单的锁顺序死锁
* @param fromAccount 转出账户
* @param toAccount 转入账户
* @param amount 转账金额
*/
public void transferMoney(Account fromAccount, Account toAccount, int amount)
throws InsufficientResourcesException {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance() < 0) {
throw new InsufficientResourcesException();
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
class Account {