精诚所至,金石为开。———《后汉书·广陵思王荆传》
意思是人的诚心所到,能感动天地,使金石为之开裂。比喻只要专心诚意去做,什么疑难问题都能解决。
两个线程试图通过不同的顺序获取多个相同的锁。如果请求的顺序不相同,那么会出现循环的锁依赖现象,产生死锁。但是如果保证同时请求锁L和锁M的每一个线程,都是按照从 L 到 M 的顺序,那么就不会发生死锁了。
举个例子说明一下,让我们更加直观的了解顺序死锁问题,请看下面代码:
public class ThreadDeadLockTest{
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) {
doSomething();
}
}
}
}
如果一个线程调用了 leftRight, 另一个线程调用了 rightLeft,这样的交替运行,那么它们会发生死锁。线程 A 拿到了left锁,而线程 B 拿到了 right 锁,这个时候就陷入了相互等待的循环了。
解决的办法是,如果所有线程以通用的固定秩序获得锁,程序就不会出现锁顺序死锁问题了。
然而,有些时候并不是那么容易避免顺序死锁的发生。比如,下面这个代码:
public class ThreadDeadLockTest{
public void transferMoney(Account fromAccount, Account toAccount, Amount money) {
synchronized (fromAccount) {
synchronized (toAccount) {
if(fromAccount < money) {
throw new InsufficientFundsException();
} else {
fromAccount.debit(money);
toAccount.credit(money);
}
}
}
}
}
针对上面程序,如果两个线程同时调用transferMoney, 一个从 A 向 B 转账,另一个从 B 向 A 转账,那么就会发生死锁。如下代码调用所示:
transferMoney(myAccount, yourAccount, 10);
transferMoney(yourAccount, myAccount, 20);
可以看出,锁的顺序就是参数的顺序,它超出了我们的控制。那么,有么有办法来制定一个锁的规范,保证锁的顺序是一致的呢?
答案是肯定的,现在我们制定一种锁的顺序,并在整个应用程序都遵循它。我们可以使用
System.identityHashCode
的方式,它返回 Object.hashCode, 唯一一个值。但是,我们还得想如何规避哈希值冲突产生的问题。哈希冲突,我们可以再引入一个锁,当哈希冲突的时候,必须获取这个锁,从而保证一次只有一个线程执行哈希冲突的情况。
请看下面代码,保证了锁顺序的一致,避免了死锁的发生。
public class ThreadTest{
//额外锁,针对哈希值冲突的情况
private static final Object tieLock = new Object();
public void transferMoney(Account fromAccount, Account toAccount, Amount money) {
class Helper{
public void transfer() {
if(fromAccount < money) {
throw new InsufficientFundsException();
} else {
fromAccount.debit(money);
toAccount.credit(money);
}
}
}
//或许哈希值
int fromHashCode = System.identityHashCode(fromAccount);
int toHashCode = System.identityHashCode(toAccount);
if(fromHashCode < toHashCode) {
synchronized (fromAccount) {
synchronized (toAccount) {
new Helper().transfer();
}
}
} else if(fromHashCode > toHashCode) {
synchronized(toAccount) {
synchronized(fromAccount) {
new Helper().transfer();
}
}
} else {
synchronized(tieLock) {
synchronized(fromAccount) {
synchronized(toAccount) {
new Helper().transfer();
}
}
}
}
}
}
上面程序就保证了锁顺序的一致性,从而规避了锁顺序死锁的情况。很多时候不易察觉这种动态的锁顺序,我们需要更加细心的使用多个锁,尽早的制定锁顺序规范。