如何避免死锁--JCIPC10读书笔记
程序员文章站
2022-03-02 10:33:48
...
[本文是我对Java Concurrency In Practice C10的归纳和总结. 转载请注明作者和出处, 如有谬误, 欢迎在评论中指正. ]
如果多个线程以不同的顺序持有多个锁, 可能发生死锁:
public class AccountTrans { public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAccount.debit(amount); toAccount.credit(amount); } } } } }
transferMoney方法先后锁定fromAccount和toAccount对象. 如果2个线程以如下的方式调用transferMoney方法:
A: transferMoney(myAccount, yourAccount, 10);
B: transferMoney(yourAccount, myAccount, 20);
死锁有可能就会发生.
关键在于需要保证以相同的顺序获取多个锁:
public class AccountTrans { // 额外的锁 private static final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException { class Helper { public void transfer() throws InsufficientFundsException { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); } } } // 计算fromAcct和toAcct的hashCode值 int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); // 根据hashCode值确定获取锁的顺序 if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } else { // 当hashCode值相同时, 无法确定fromAcct和头Acct锁的获取顺序, 因此增加额外的锁 synchronized (tieLock) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } } } }
open call
所谓open call是指在未持有锁时调用外部方法. 持有锁的时候调用外部方法, 如果被调用的方法需要获取其他的锁, 可能带来死锁的风险. 如果被调用的方法发生阻塞, 当前线程将长时间持有锁, 其他等待获取该锁的线程就会被阻塞.
因此我们应该尽量在未持有锁的时候进行方法的调用.
资源死锁
比如线程A持有数据库D1的连接, 并等待获取数据库D2的连接. 而线程B持有数据库D2的连接, 并等待获取数据库D1的连接. 此时就发生了死锁.
资源死锁的另一种形式是线程饥饿死锁, 参见第八章.
避免死锁
1. 尽量不要同时持有多个锁.
2. 如果必须同时持有多个锁, 那么保证以一致的顺序获取锁.
3. 尽量在未持有锁的情况下进行方法的调用(open call).