线程死锁操作的一般情况都是,等待拿到某一个锁来进行操作或者说某一个资源,如果一直拿不到的话,那么就一直阻塞,导致程序无法正常结束或者终止.
有一个非常经典的问题可以说明这个现象(哲学家吃饭问题),5个哲学家去吃饭,坐在一张圆桌旁,
他们有5根筷子,并且每两个人中间放一根筷子,哲学家们时而思考,时而进餐,每个人都需要一双筷子才能吃到东西,并在吃完后将筷子放回原处继续思考。
一般情况下,每个人都迅速的拿到自己左边的筷子,然后尝试拿右边的筷子,但是同时不放下自己手上的筷子,而是等待其他人放下他的筷子,这就产生了死锁。 有一些管理筷子的算法,能够使每个人都吃到东西。比如 我拿到了筷子之后,尝试拿另外一根筷子时发现其他人已经拿走了,我就放弃自己手中的筷子,并且过段时间后在去尝试。
这样的做法在性能上有一定的损耗。我们先来看下面这段程序,银行帐户之间转账。
1 public static void transferMoney(final AccountUser a1 , final AccountUser a2, final double money) { 2 synchronized (a1) { 3 synchronized (a2) { 4 // do something. 5 if (a2.getBalance() - money > 0) { 6 a2.setBalance(a2.getBalance() - money); 7 a1.setBalance(a1.getBalance() + money); 8 } else { 9 throw new RuntimeException("Error"); 10 } 11 } 12 } 13 }
假设 现在有这样一个情况,A用户向B用户转账,同时B用户向A用户转账.
这种加锁的顺序,很可能会导致死锁。这种情况称为Lock-Ordering Deadlock(锁顺序死锁). 线程1 先拿到A用户的锁,线程2 拿到B用户锁,线程1等着拿B用户的锁,线程2等着拿A用户的锁,他们如果不进行协调的话,那么他们就会一直等着,导致转账无法正常完成。
我们可以通过控制拿锁的顺序来避免这种情况,将程序修改成下面这种方式
public static void transferMoney(final AccountUser a1 , final AccountUser a2, final double money) { int fromHash = System.identityHashCode(a1); int toHash = System.identityHashCode(a2); if (fromHash > toHash) { synchronized (a1) { synchronized (a2) { // do something. if (a2.getBalance() - money > 0) { a2.setBalance(a2.getBalance() - money); a1.setBalance(a1.getBalance() + money); } else { throw new RuntimeException("Error"); } } } } else if (fromHash < toHash) { synchronized (a2) { synchronized (a1) { // do something. if (a2.getBalance() - money > 0) { a2.setBalance(a2.getBalance() - money); a1.setBalance(a1.getBalance() + money); } else { throw new RuntimeException("Error"); } } } } else { synchronized (lock) { synchronized (a1) { synchronized (a2) { // do something. if (a2.getBalance() - money > 0) { a2.setBalance(a2.getBalance() - money); a1.setBalance(a1.getBalance() + money); } else { throw new RuntimeException("Error"); } } } } } }
通过hashCode的值来简单的判断,从而避免死锁发生的可能性。如果出现A转账给A的情况,hashCode一样的话,那么我们就在加一个额外的锁来控制他。