线程死锁操作的一般情况都是,等待拿到某一个锁来进行操作或者说某一个资源,如果一直拿不到的话,那么就一直阻塞,导致程序无法正常结束或者终止.
有一个非常经典的问题可以说明这个现象(哲学家吃饭问题),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一样的话,那么我们就在加一个额外的锁来控制他。
如果我们是从A -> b 那么,他们的hashcode将不一样,线程首先获得的lock是A对象,并且同时有B ->A 那么,这个线程获得的锁也将是A 所以就会造成阻塞效果。
对于java的锁顺序的控制,来避免死锁的情况发生,除了上述的方法之外。 我们可以采用其他的策略来进行控制,例如 采用上面的主动放弃自己拥有的锁的策略, 如果线程a 拿到了userA的锁,同时还需要获取其他的锁,我们可以让线程a尝试的去获取另外的锁,如果没有获取到那么我们大方一些让出自己的锁,同时让自己休息一段时间,在重新去获取UserA(自己的第一把锁),再次尝试获取其他的锁。 这样我们就可以让出CPU等资源。
我们考虑另外一种方案,我们结合两种策略(hashCode + 自动释放锁),如果线程a 和线程b都在获取同一些锁的时候,我们让线程A自己主动放弃,过段时间在去获取锁。