Java多线程——死锁
当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么他永远被阻塞,当线程A持有锁L并想获得锁M的同时,线程B持有锁M并同时尝试获得锁L时,那么两个线程将永远的等待下去,这中情况就是简单的死锁的形式,其中多个线程由于存在环路的锁依赖关系而永远的等待下去,那么就存在一个死锁。
1、锁顺序死锁
下面是顺序锁的一个列子,代码如下:
1 package deadLock; 2 3 public class LeftRightDeadLock { 4 private final Object left = new Object(); 5 private final Object right = new Object(); 6 7 public void leftRight() throws Exception{ 8 synchronized (left) { 9 Thread.sleep(2000); 10 synchronized (right) { 11 System.out.println("left to right"); 12 } 13 } 14 } 15 16 17 public void rightLeft() throws Exception{ 18 synchronized (right) { 19 Thread.sleep(2000); 20 synchronized (left) { 21 System.out.println("right to left"); 22 } 23 } 24 } 25 26 }
1 package deadLock; 2 3 public class LeftRightThread extends Thread { 4 5 private LeftRightDeadLock d; 6 public LeftRightThread(LeftRightDeadLock d){ 7 this.d = d; 8 } 9 @Override 10 public void run() { 11 try{ 12 d.leftRight(); 13 }catch(Exception ex){ 14 ex.printStackTrace(); 15 } 16 } 17 18 }
1 package deadLock; 2 3 public class RightLeftThread extends Thread { 4 5 private LeftRightDeadLock d; 6 public RightLeftThread(LeftRightDeadLock d){ 7 this.d = d; 8 } 9 @Override 10 public void run() { 11 try{ 12 d.rightLeft(); 13 }catch(Exception ex){ 14 ex.printStackTrace(); 15 } 16 } 17 18 }
1 package deadLock; 2 3 public class Main { 4 public static void main(String[] args) { 5 LeftRightDeadLock d = new LeftRightDeadLock(); 6 LeftRightThread t1 = new LeftRightThread(d); 7 RightLeftThread t2 = new RightLeftThread(d); 8 t1.start(); 9 t2.start(); 10 } 11 }
线程t1持有left的锁,并尝试获取得right的锁,而线程t2持有right的锁,并尝试获得left的锁,故产生死锁。产生死锁的原因是:两个线程试图以不同的顺序来获得相同的锁,如果按照相同的顺序来请求锁,那么就不会出现循环的加锁依赖性,因此也就不会产生死锁。
如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁的问题
2、动态的锁顺序死锁
有时候,并不能清楚的知道是否在锁顺序上有足够的控制权来避免死锁的发生,看如下转账的代码。
1 public class TransferAccounts { 2 public void transferMoney(Account fromAccount, Account toAccount, double amount) throws Exception{ 3 synchronized (fromAccount) { 4 synchronized (toAccount) { 5 if(fromAccount.getBalance() - amount < 0){ 6 throw new Exception(); 7 } 8 else{ 9 fromAccount.setBalance(amount); 10 toAccount.add(amount); 11 } 12 } 13 } 14 } 15 }
1 public class Account { 2 3 //金额 4 private double balance; 5 6 public double getBalance() { 7 return balance; 8 } 9 10 public void setBalance(double balance) { 11 this.balance = balance; 12 } 13 public void add(double amount){ 14 balance += amount; 15 } 16 public void subtra(double amount){ 17 balance -= amount; 18 } 19 20 }
上面代码是资金从一个账户转到另一个账户的简单实现,在开始转账之前要获得这两个Account对象的锁,以确保通过原子方式来更新两个账户的中余额。所有的线程似乎都是按照顺序锁来获得锁,但是事实上锁的顺序取决于转给函数transferMoney参数顺序,而这些参数又取决于外部的输入,如果两个线程同时调用transferMoney,其中一个线程从X向Y转账,另一个线程从Y向X转账,有可能发生死锁:
线程A:transferMoney(xAccount, yAccount);
线程B:transferMoney(yAccount, xAccount);
为了防止这种情况发生,必须按照顺序来获取锁。如下代码:
1 public class TransferAccounts { 2 private static final Object tieLock = new Object(); 3 4 public void transfer(Account fromAccount, Account toAccount, 5 double amount) throws Exception { 6 if (fromAccount.getBalance() - amount < 0) { 7 throw new Exception(); 8 } else { 9 fromAccount.setBalance(amount); 10 toAccount.add(amount); 11 } 12 } 13 public void transferMoney(Account fromAccount, Account toAccount, 14 double amount) throws Exception{ 15 int fromHash = fromAccount.hashCode(); 16 int toHash = toAccount.hashCode(); 17 if(fromHash < toHash){ 18 synchronized (fromAccount) { 19 synchronized (toAccount) { 20 transfer(fromAccount, toAccount, amount); 21 } 22 } 23 }else if(fromHash > toHash){ 24 synchronized (toAccount) { 25 synchronized (fromAccount) { 26 transfer(fromAccount, toAccount, amount); 27 } 28 } 29 }else { 30 synchronized (tieLock) { 31 synchronized (fromAccount) { 32 synchronized (toAccount) { 33 transfer(fromAccount, toAccount, amount); 34 } 35 } 36 } 37 } 38 } 39 40 }
在极少数的情况下,两个对象可能拥有相对的hashCode值,这时必须通过添加额外的锁来,在获得两个Account的锁之前,必须先获得这个额外的锁。从而消除死锁的发生。
3、死锁的避免与诊断
(1) 如果一个程序每次至多只能获取一个锁,那么就不会产生锁顺序死锁。
(2) 如果必须获取多个锁,那么设计时必须考虑锁的顺序,如果按照固定的顺序来获取锁,就不会发生锁顺序死锁。
(3)支持定时锁,例如显示使用Lock类中的定时tryLock功能来代替内置锁,显示锁可以指定一个超时时限,在等待超过某个时间后tryLock会返回一个失败的信息。