初次接触死锁的概念是大学的一门课程《操作系统原理》中描述的“哲学家进餐”问题。操作系统中,由于各个进程共享系统资源而可能出现死锁问题。同样java多线程环境下,也存在资源共享导致的死锁问题。当一组java线程发生死锁时,程序有可能就此阻塞,而无法正常结束。Java应用程序中,我们使用加锁机制来确保线程的安全,但是如果过度地使用加锁,可能导致锁顺序死锁问题。
整理《java并发编程实践》一书中锁顺序死锁的分析,及代码示例验证如下。
程序中需要同时获取多个锁的时候,加锁的顺序不同,可能会导致死锁问题。一种简单的锁顺序死锁的代码如下:
public class LeftRightDeadLock { private final Object left = new Object(); private final Object right = new Object(); public void leftRight(){ synchronized (left) { synchronized (right) { //doSomethoing(); } } } public void rightLeft(){ synchronized (right) { synchronized (left) { //doSomethoing(); } } } }以上代码存在死锁风险:当一个线程调用leftRight方法获取了left锁后,而另一个线程同时调用了rightLeft方法获取了right锁,那么此时他们都会陷入等待另一个锁的阻塞过程,而导致死锁。因为他们的操作是交错进行的,而加在相同对象的锁的顺序也是交错的,这就容易导致死锁的发生。破解的方法就是:所有的线程以固定的顺序来获取相同的锁,那么程序中就不会出现锁顺序死锁问题。
顶层金额抽象类Amount,实现comparable以便转账之前的数据校验操作;它的子类DallarAmount。
/** * 顶层金额抽象类:包含币种和余额两个属性 * @author bh */ public abstract class Amount implements Comparable<Amount>{ public abstract BigDecimal getBalance(); public abstract void setBalance(BigDecimal balance); public abstract Currency getCurrency(); @Override public int compareTo(Amount o) { if(o==null){ throw new NullPointerException("null arg."); } if(this.getBalance()==null||o.getBalance()==null){ throw new NullPointerException("null arg."); } return this.getBalance().compareTo(o.getBalance()); } }
/** * 美元类 * @author bh */ public class DollarAmount extends Amount{ private BigDecimal balance; private Currency currency = Currency.getInstance(Locale.US); public DollarAmount(BigDecimal balance){ this.balance = balance; } public BigDecimal getBalance() { return balance; } public void setAmount(BigDecimal amount) { this.balance = amount; } public Currency getCurrency() { return currency; } public void setBalance(BigDecimal balance) { this.balance = balance; } }Account类,提供账户的借贷方法。
/** * 账户类:包括金额,借贷及获取余额方法 * @author bh */ public class Account { private String id; private Amount balance; public Account(Amount amount,String id){ this.balance = amount; this.id = id; } public Amount getBalance() { return balance; } public void setBalance(Amount balance) { this.balance = balance; } public void debit(Amount amount){ if(this.balance==null||amount==null||amount.getBalance()==null){ return; } System.out.println(id+" 支出金额"+amount.getBalance()); //修正账户余额:本账户减去借方金额 BigDecimal current = this.balance.getBalance(); BigDecimal now = current.subtract(amount.getBalance()); this.balance.setBalance(now); } public void credit(Amount amount){ if(this.balance==null||amount==null||amount.getBalance()==null){ return; } System.out.println(id+" 收入金额"+amount.getBalance()); //修正账户余额:本账户加贷方金额 BigDecimal current = this.balance.getBalance(); BigDecimal now = current.add(amount.getBalance()); this.balance.setBalance(now); } public String getId() { return id; } public void setId(String id) { this.id = id; } }转账功能提供者,在执行转账之前获取两个账户的锁:
/** * 同时获取两个账户的锁 * @author bh */ public class AccountHelper { public void transferMoney(final Account fromAcct, final Account toAcct, final Amount amount){ //参数校验 if(fromAcct==null||toAcct==null||amount==null){ throw new IllegalArgumentException("null arg."); } //余额校验 if(fromAcct.getBalance().compareTo(amount)<0){ throw new IllegalArgumentException(fromAcct.getId()+"账户余额不足"); } synchronized (fromAcct) { synchronized (toAcct) { this.transfer(fromAcct, toAcct, amount); } } } //transfer对两个账户的操作必须是原子的完成 private void transfer(final Account fromAcct, final Account toAcct, final Amount amount){ System.out.println("Thread "+Thread.currentThread().getName()+" do transfer."); fromAcct.debit(amount); toAcct.credit(amount); } }上述代码看似没有问题,但是它确有死锁的问题,虽然线程看似以相同的加锁方式先对from加锁,再对to账户加锁,但事实上锁的顺序是动态的,取决于传递给这个方法的参数的顺序,而这些参数的顺序又取决于外部输入。编写测试代码,同时启动四个线程交替执行transferMoney方法,而参数传递顺序则以相反的顺序。代码如下:
public class MainTest { public static void main(String[] args) { Amount amFromAcc = new DollarAmount(new BigDecimal(2000)); Amount amToAcc = new DollarAmount(new BigDecimal(1000)); final AccountHelper h = new AccountHelper(); final Account fromAcc = new Account(amFromAcc,"zhang_3"); final Account toAcc = new Account(amToAcc,"wang_5"); final Amount amToTran = new DollarAmount(new BigDecimal(1)); Thread t1 = new Thread(new Runnable(){ @Override public void run() { h.transferMoney(fromAcc, toAcc, amToTran); } }); Thread t4 = new Thread(new Runnable(){ @Override public void run() { h.transferMoney(fromAcc, toAcc, amToTran); } }); Thread t2 = new Thread(new Runnable(){ @Override public void run() { h.transferMoney(toAcc, fromAcc, amToTran); } }); Thread t3 = new Thread(new Runnable(){ @Override public void run() { h.transferMoney(toAcc, fromAcc, amToTran); } }); t1.start(); t2.start(); t3.start(); t4.start(); } }反复执行该方法,前三次恰巧能够正常运行;但是第四次执行的时候,就遭遇了死锁问题,只有两个线程顺序完成了操作,而另外两个线程陷入死锁状态,这个程序一直无法结束,测试结果如下:
因为我们无法控制参数的顺序,所以必须定义一种固定的锁的顺序,使得程序的锁定顺序不受参数的影响。该书中给出的解决办法是:比较对象的hashCode,以此固定一种顺序,当两个对象拥有相同的哈希值时,再使用加时赛锁固定加锁的顺序。AccountHelper破解动态顺序死锁的代码如下:
/** * 根据某种规则设置锁的获取顺序,避免锁顺序死锁问题 * @author bh * */ public class AccountHelper { //加时赛锁 private final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final Amount amount){ //参数校验 if(fromAcct==null||toAcct==null||amount==null){ throw new IllegalArgumentException("null arg."); } //余额校验 if(fromAcct.getBalance().compareTo(amount)<0){ throw new IllegalArgumentException(fromAcct.getId()+"账户余额不足"); } //根据对象的hash值随机设置加锁顺序 int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if(fromHash<toHash){ synchronized (fromAcct) { synchronized (toAcct) { this.transfer(fromAcct, toAcct, amount); } } }else if(fromHash>toHash){ synchronized (toAcct) { synchronized (fromAcct) { this.transfer(fromAcct, toAcct, amount); } } }else{ //碰巧相对时,先获取加时赛锁 synchronized (tieLock) { synchronized (fromAcct) { synchronized (toAcct) { this.transfer(fromAcct, toAcct, amount); } } } } } //transfer对两个账户的操作必须是原子的完成 private void transfer(final Account fromAcct, final Account toAcct, final Amount amount){ System.out.println("Thread "+Thread.currentThread().getName()+" do transfer."); fromAcct.debit(amount); toAcct.credit(amount); } }