目录
解决方案:定义锁的顺序,并且整个应用中都按照这个顺序来获取锁。
JAVA中死锁的定义:
在JAVA中我们会使用加锁机制来保证线程的安全,但如果过度使用加锁操作,可能会出现死锁的情况。举个例子:当一个线程永远的持有一个锁, 并且其他线程都尝试获取这个锁时,那么它们将永远被阻塞。在线程A持有锁L并想获取锁M的同时,线程B持有锁M并尝试获取锁L,那么这两个线程将永远地等待下去。这种就是最简单的死锁形式(或者叫做“抱死”)。
如图:死锁的原因就是A、B线程试图以不同的顺序来获取相同的锁。所以,所有的线程以固定的顺序来获取锁,那么在程序中就可以尽量避免出现锁顺序导致的死锁问题。
死锁实例
1)实例业务场景
我以一个经典的转账案例来进行说明,我们知道转账就是将资金从一个账户转入另一个账户。在开始转账之前,首先需要获取这两个账户的对象锁,以确保通过原子方式来更新这两个账户的余额,同时又不破坏一些不变形条件,如:转账的余额不能为负数。
代码如下:
//动态的锁的顺序死锁 public class DynamicOrderDeadlock { public static void transferMoney(Account fromAccount,Account toAccount,int amount,int from_index,int to_index) throws Exception { System.out.println("账户 "+ from_index+"~和账户~"+to_index+" ~请求锁"); synchronized (fromAccount) { System.out.println(" 账户 >>>"+from_index+" <<<获得锁"); synchronized (toAccount) { System.out.println(" 账户 "+from_index+" & "+to_index+"都获得锁"); if (fromAccount.compareTo(amount) < 0) { throw new Exception(); }else { fromAccount.debit(amount); toAccount.credit(amount); } } } } static class Account { private int balance = 100000;//这里假设每个人账户里面初始化的钱 private final int accNo; private static final AtomicInteger sequence = new AtomicInteger(); public Account() { accNo = sequence.incrementAndGet(); } void debit(int m) throws InterruptedException { Thread.sleep(5);//模拟操作时间 balance = balance + m; } void credit(int m) throws InterruptedException { Thread.sleep(5);//模拟操作时间 balance = balance - m; } int getBalance() { return balance; } int getAccNo() { return accNo; } public int compareTo(int money) { if (balance > money) { return 1; }else if (balance < money) { return -1; }else { return 0; } } } }
public class DemonstrateDeadLock { private static final int NUM_THREADS = 5; private static final int NUM_ACCOUNTS = 5; private static final int NUM_ITERATIONS = 100000; public static void main(String[] args) { final Random rnd = new Random(); final Account[] accounts = new Account[NUM_ACCOUNTS]; for(int i = 0;i < accounts.length;i++) { accounts[i] = new Account(); } class TransferThread extends Thread{ @Override public void run() { for(int i = 0;i < NUM_ITERATIONS;i++) { int fromAcct = rnd.nextInt(NUM_ACCOUNTS); int toAcct =rnd.nextInt(NUM_ACCOUNTS); int amount = rnd.nextInt(100); try { DynamicOrderDeadlock.transferMoney(accounts[fromAcct],accounts[toAcct], amount,fromAcct,toAcct); //InduceLockOrder.transferMoney(accounts[fromAcct],accounts[toAcct], amount); //InduceLockOrder2.transferMoney(accounts[fromAcct],accounts[toAcct], amount); }catch (Exception e) { System.out.println("发生异常-------"+e); } } } } for(int i = 0;i < NUM_THREADS;i++) { new TransferThread().start(); } } }
打印结果如下:
注意:这里的结果是我把已经执行完的给删除后,只剩下导致死锁的请求.
从打印结果的图片中可以的得到结论:由于我们无法控制transferMoney中的参数的顺序,而这些参数顺序取决于外部的输入。所以两个线程同时调用transferMoney,一个线程从X向Y转账,另一个线程从Y向X转账,那么就会发生互相等待锁的情况,导致死锁。
解决问题方案:定义锁的顺序,并且整个应用中都按照这个顺序来获取锁。
方案一
使用System.identityHashCode方法,该方法返回有Object.hashCode返回的值,此时可以通过某种任意方法来决定锁的顺序。但是在极少数情况下,两个对象可能拥有相同的散列值,在这种情况下,通过给公共变量加锁来实现给锁制定顺序。所以这种方法也是用最小的代价,换来了最大的安全性。
具体代码如下://通过锁顺序来避免死锁 public class InduceLockOrder { private static final Object tieLock = new Object(); public static void transferMoney(final Account fromAcct, final Account toAcct, final int amount) throws Exception { class Helper { public void transfer() throws Exception { if (fromAcct.compareTo(amount) < 0) { throw new Exception(); } else { fromAcct.debit(amount); toAcct.credit(amount); } } } int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } else { synchronized (tieLock) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } } } static class Account { private int balance = 100000; public Account() { } void debit(int m) throws InterruptedException { Thread.sleep(5); balance = balance + m; } void credit(int m) throws InterruptedException { Thread.sleep(5); balance = balance - m; } int getBalance() { return balance; } public int compareTo(int money) { if (balance > money) { return 1; }else if (balance < money) { return -1; }else { return 0; } } } }
方案二
在Account中包含一个唯一的,不可变的,值。比如说账号等。通过对这个值对对象进行排序。
具体代码如下:public class InduceLockOrder2 { public static void transferMoney(final Account fromAcct, final Account toAcct, final int amount) throws Exception { class Helper { public void transfer() throws Exception { if (fromAcct.compareTo(amount) < 0) { throw new Exception(); } else { fromAcct.debit(amount); toAcct.credit(amount); } } } int fromHash = fromAcct.getAccNo(); int toHash = toAcct.getAccNo(); if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } } static class Account { private int balance = 100000; private final int accNo; private static final AtomicInteger sequence = new AtomicInteger(); public Account() { accNo = sequence.incrementAndGet(); } void debit(int m) throws InterruptedException { Thread.sleep(6); balance = balance + m; } void credit(int m) throws InterruptedException { Thread.sleep(6); balance = balance - m; } int getBalance() { return balance; } int getAccNo() { return accNo; } public int compareTo(int money) { if (balance > money) { return 1; }else if (balance < money) { return -1; }else { return 0; } } } }
2)实例业务场景本例来自Thinking in Java,具体的业务场景如下:
有5个哲学家去就餐,但是就只有5根筷子,这5个哲学家围坐在一起,每个哲学家的左边和右边都有一根筷子。哲学家可能在思考,也可能在就餐。哲学家要就餐的话必须获取到左边和右边两根筷子,如果这个哲学家的左边或者右边的筷子已经正在被其他哲学家使用,则要就餐的哲学家必须等待其他哲学家就餐完毕释放筷子!
具体见下图:图中圆圈表示哲学家,线条表示筷子!
具体代码如下:在本例中筷子属于竞争资源,加锁的方法肯定在筷子对应的类中,定义chopstick类,表示筷子。
package thread.test.deadLock; /** * 筷子类 */ public class Chopstick { //筷子的使用状态,true-使用,false-未使用 private Boolean taken = false; /** * 使用筷子的方法 * * @throws InterruptedException */ public synchronized void take() throws InterruptedException { //如果筷子的状态是使用,则等待 while (taken) { wait(); } //将筷子的状态改成使用 taken = true; } /** * 放下筷子方法 */ public synchronized void drop() { //将筷子的状态改成未使用 taken = false; //通知其他哲学家可以使用这根筷子 notifyAll(); } }
在本例中哲学家属于任务,哲学家类实现Runnable接口使用筷子即竞争资源,在run方法中哲学家先思考片刻,然后先拿右边的筷子,后拿左边的筷子,如果都拿到了那么哲学家开始吃饭,然后先释放右边的筷子再释放左边的筷子!如果不能拿到左右两根筷子,那么哲学家持有一根筷子,等待另一根筷子被其他哲学家释放!代码如下:
package thread.test.deadLock; import java.util.concurrent.TimeUnit; /** * 哲学家类 */ public class Philosopher implements Runnable { //哲学家左边的筷子 private Chopstick left; //哲学家右边的筷子 private Chopstick right; //哲学家编号 private final int id; //哲学家思考 private final int ponderFactor; public Philosopher(int id, int ponderFactor, Chopstick left, Chopstick right) { this.id = id; this.ponderFactor = ponderFactor; this.left = left; this.right = right; } /** * 哲学家思考,思考时间ponderFactor * * @throws InterruptedException */ private void thinking() throws InterruptedException { //如果不思考直接跳过 if (ponderFactor == 0) { return; } //哲学家思考ponderFactor TimeUnit.MILLISECONDS.sleep(ponderFactor); } /** * run方法,提交的任务如果不打断的话且没有死锁的话会一直进行下去 */ @Override public void run() { try { while (!Thread.interrupted()) { //哲学家先思考片刻 System.out.println(this + " is thinking!"); thinking(); //拿右边的筷子 System.out.println(this + " taking right chopstick!"); right.take(); System.out.println(this + " taked right chopstick!"); //拿左边的筷子 System.out.println(this + " taking left chopstick!"); left.take(); System.out.println(this + " taked left chopstick!"); //吃饭 System.out.println(this + " is eating!"); //放下筷子 right.drop(); left.drop(); } } catch (Exception e) { e.printStackTrace(); } } public String toString() { return "Philosopher" + id; } }
显然,定义的Philosopher很容易造成死锁,如果每个哲学家都是先拿右手边的筷子或者先拿左手边的筷子,那么5个哲学家都是持有一个筷子并等待其他哲学家释放筷子!此时,每个任务就会持有其他任务等待的锁,形成一个相互等待的连续循环,从而造成死锁!
下面演示下死锁的示例代码,代码如下:
package thread.test.deadLock; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 死锁问题测试 */ public class PhilosopherDeadLockTest { public static void main(String... args) { //创建线程池 ExecutorService executorService = Executors.newCachedThreadPool(); //创建5根筷子对象 Chopstick[] chopsticks = new Chopstick[5]; for (int i = 0; i < 5; i++) { chopsticks[i] = new Chopstick(); } //提交 for (int i = 0; i < 5; i++) { executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5])); } executorService.shutdown(); } }
运行下,打印结果如下:
由上图可知,程序处于阻塞状态,打印了5条Philosopher* taked right chopstick,表示这5个任务都先拿到了右边的筷子,打印了5条Philosopher* taking left chopstick,大家都没有拿到左边的筷子,等处于等待的状态!
由上图可知,程序处于阻塞状态,打印了5条Philosopher* taked right chopstick,表示这5个任务都先拿到了右边的筷子,打印了5条Philosopher* taking left chopstick,大家都没有拿到左边的筷子,等处于等待的状态!
解决方案:
知道了造成死锁的原因了,那么解决死锁问题就是让代码逻辑不会同时满足上述4条即可!
在本例中,可以让前4个哲学家先获取右边的筷子然后获取左边的筷子,让最后一个哲学家先获取左边的筷子然后获取右边的筷子,这样第5个哲学家在获取左边的筷子的时候就会被阻塞,此时有一个筷子处于未被占用的状态,第1个哲学家就可以获取到,吃完后就会释放左右两根筷子,从而不会造成死锁问题!这是通过打破上述造成死锁原因第4条的方式解决死锁问题,具体代码如下:
package thread.test.deadLock; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 解决死锁问题测试 */ public class PhilosopherFixedDeadLockTest { public static void main(String... args) { //创建线程池 ExecutorService executorService = Executors.newCachedThreadPool(); //创建5根筷子对象 Chopstick[] chopsticks = new Chopstick[5]; for (int i = 0; i < 5; i++) { chopsticks[i] = new Chopstick(); } //提交 /** * 解决死锁: * 前4个哲学家先拿右边筷子,后拿左边筷子 * 最后一个哲学家先拿左边筷子,后拿右边筷子 */ for (int i = 0; i < 5; i++) { if (i < 4) { executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5])); } else { executorService.submit(new Philosopher(i, 5, chopsticks[0], chopsticks[5])); } } executorService.shutdown(); } }
总结造成死锁的原因:
造成死锁的原因有如下4条,且必须同时满足:
1)互斥条件:任务使用的资源中至少有一个是不能共享的,本例中筷子都不能共享,需要竞争,筷子的使用和释放方法都是用了synchronized关键字修饰!
2)至少有一个任务它必须持有一个资源且正在等待获取一个当前正在被别的任务持有的资源,本例中哲学家必须持有一根筷子且其他筷子都被其他哲学家持有!
3)资源不能被任务抢占,任务必须把资源释放当做普通事件,资源只能被占用的资源释放后才能被其他任务获取到!
4)必须有循环等待,这时一个任务等待其他任务释放资源,其他任务又在等待另外一个任务释放资源,且直到最后有一个任务在等待第一个任务释放资源,使得大家都被锁住!
如何防患?
1)尽量避免使用多个锁(如果有可能的话)。
2)规范的使用多个锁,并设计好锁的获取顺序。
3)随用随放。即是,手里有锁,如果还要获得别的锁,必须释放全部资源才能各取所需。
4)规范好循环等待条件。比如,使用超时循环等待,提高程序可控性。
定位死锁(解决方案)
最常见的方式是利用jstack等工具获取线程,然后定位互相之间的依赖关系,然后找到死锁。如果是比较明显的死锁,是可以直接定位到了,类似JConsole,甚至于通过图形界面进行有限的死锁检测。排查方案:java程序死锁,3种方式快速找到死锁代码 - 路人甲Java - 博客园 (cnblogs.com)
心得:
Java对死锁没有提供语言层面上的支持,只能通过程序员通过仔细的设计来避免死锁!所以在设计多线程程序时,应该考虑造成死锁的4个条件,绝对不能让上述4个条件同时满足!