什么是死锁?所谓死锁,是指多个线程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,多个线程都无法继续执行,总而导致死锁现象。
下面演示一种必定发生死锁的情况
package DeadLock; /** * @program:多线程和IO * @descripton:打开注释,就会死锁。 * @author:ZhengCheng * @create:2021/9/30-21:08 **/ public class DeadLockDemo{ static Object lock1 =new Object(); static Object lock2 =new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new a()); Thread t2 = new Thread(new b()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("end"); } static class a implements Runnable{ @Override public void run() { synchronized (lock1){ System.out.println("getLock1"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ /*try { lock1.wait(); } catch (InterruptedException e) { e.printStackTrace(); }*/ System.out.println("getLock1+Lock2"); } } } } static class b implements Runnable{ @Override public void run() { synchronized (lock2){ System.out.println("getLock2"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1){ /*try { lock2.wait(); } catch (InterruptedException e) { e.printStackTrace(); }*/ System.out.println("getLock2+Lock1"); } } } } }
上面演示的就是一种死锁的情况。死锁虽然发生的概率小,但是一旦死锁,就会出现很大的问题。我们用两个转账的案例,来模拟现实中可能出现的死锁情况。
第一种是两个人互相转账
package DeadLock; /** * @program:多线程和IO * @descripton:转账时遇到死锁,一旦打开注释 * @author:ZhengCheng * @create:2021/9/30-21:41 **/ public class TransferMoney implements Runnable{ int flag = 1; static Account a = new Account(500); static Account b= new Account(500); public static void main(String[] args) throws InterruptedException { TransferMoney r1 = new TransferMoney(); TransferMoney r2 = new TransferMoney(); r1.flag = 1; r2.flag = 0; Thread thread1 = new Thread(r1); Thread thread2 = new Thread(r2); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(a.balance); System.out.println(b.balance); System.out.println("end"); } @Override public void run() { if (flag == 1){ transferMoney(a,b,200); } if (flag == 0){ transferMoney(b,a,300); } } public static void transferMoney(Account from , Account to ,int money ){ synchronized (from){ try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (to){ if (from.balance - money < 0){ System.out.println("余额不足"); return; }else { from.balance -= money; to.balance += money; System.out.println("交易成功,余额增加"+money); } } } } public static class Account{ int balance; public Account(int balance) { this.balance = balance; } } }
第二种是很多个人随机转账。(开启多个线程)
package DeadLock; import DeadLock.TransferMoney.Account; import java.util.Random; /** * @program:多线程和IO * @descripton:几千个人,开启多个线程,看是否会发生死锁,更具有普遍性 * @author:ZhengCheng * @create:2021/10/1-22:21 **/ public class TransferManyMan { private static int Num_Acct = 500; private static int Num_Money = 1000; private static int Num_Trans = 1000000; private static int Num_Threads = 20; public static void main(String[] args) { Account[] accounts = new Account[Num_Acct]; for (int i = 0; i < Num_Acct; i++) { accounts[i] = new Account(Num_Money); } Random random = new Random(); for (int i = 0 ; i< Num_Threads ; i++) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < Num_Trans; i++) { Account from_acc = accounts[random.nextInt(Num_Acct)]; Account to_acc = accounts[random.nextInt(Num_Acct)]; TransferMoney.transferMoney(from_acc,to_acc,random.nextInt(Num_Money)); } } }).start(); } } }
最后不能发现,执行上述代码,都发生了死锁现象。可见,死锁的产生,对我们日常使用以及是有极大的危害的。
从上面是三个案例,我们可以总结一下发生死锁的几个必要条件
♦ 互斥条件 ♦ 请求与保持条件 ♦ 不剥夺条件 ♦ 循环等待条件
当死锁发生以后,我们必然会想定位死锁发生的位置。
如何定位呢?
使用jstack命令行
使用ThreadMXBean(主要)
下面使用之前的Demo,我们使用ThreadMXBean来找到发生死锁的线程。
package DeadLock; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; /** * @program:多线程和IO * @descripton:打开注释,就会死锁。 * @author:ZhengCheng * @create:2021/9/30-21:08 **/ public class ThreadMXBeanDetection implements Runnable { public ThreadMXBeanDetection(int flag) { this.flag = flag; } int flag; static Object lock1 = new Object(); static Object lock2 = new Object(); public static void main(String[] args) throws InterruptedException { ThreadMXBeanDetection td1 = new ThreadMXBeanDetection(1); ThreadMXBeanDetection td2 = new ThreadMXBeanDetection(0); Thread t1 = new Thread(td1); Thread t2 = new Thread(td2); t1.start(); t2.start(); Thread.sleep(1000); ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); if (deadlockedThreads != null && deadlockedThreads.length > 0){ for (int i = 0; i < deadlockedThreads.length; i++) { ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]); System.out.println("发现了死锁:"+threadInfo.getThreadName()); } } } @Override public void run() { if (flag == 1) { synchronized (lock1) { System.out.println("getLock1"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("getLock1+Lock2"); } } } if (flag == 0) { synchronized (lock2) { System.out.println("getLock2"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("getLock2+Lock1"); } } } } }
能够找到死锁的位置,那么我们肯定要尝试去修复死锁的问题。
修复死锁的问题有三种解决方法。
1.避免死锁(通过对代码的设计,避免死锁的发生)
2.检测与恢复策略:一段时间检测是否有死锁,如果有就波多某一个资源,来打开死锁。
3.鸵鸟策略
那么下面以最经典的哲学家就餐问题,来展示修复死锁的各种方法。
首先写出哲学家就餐的demo
package DeadLock; /** * @program:多线程和IO * @descripton:哲学家就餐问题 * @author:ZhengCheng * @create:2021/10/2-21:33 **/ public class DiningTable { public static class Philosopher implements Runnable{ private Object leftChop ; private Object rightChop; public Philosopher(Object leftChop, Object rightChop) { this.leftChop = leftChop; this.rightChop = rightChop; } @Override public void run() { while (true){ try { doAction("Thinking"); synchronized (leftChop){ doAction("Get left Chop"); synchronized (rightChop){ doAction("Get right chop"); doAction("EAT"); doAction("Put down right chop"); } doAction("Put down left chop"); } } catch (InterruptedException e) { e.printStackTrace(); } } } private void doAction(String action) throws InterruptedException { System.out.println(Thread.currentThread().getName()+" "+action); Thread.sleep((long)Math.random()*10); } } public static void main(String[] args) { Philosopher[] philosophers = new Philosopher[5]; Object[] chops = new Object[philosophers.length]; for (int i = 0; i < chops.length; i++) { chops[i] = new Object(); } for (int i = 0; i < philosophers.length; i++) { Object left = chops[i]; Object right = chops[(i+1)%philosophers.length]; philosophers[i] = new Philosopher(left ,right); new Thread(philosophers[i],"哲学家"+(i+1)+"号").start(); } } }
解决哲学家就餐问题,我们有许多种方法。按照方法同样归属于最初的三种。
避免原则:通过逻辑设计,让其规避死锁的风险。只要锁构成的有向图没有成环,那么就没有死锁的风险。于是给出一个经典的解决方案,让其中一个哲学家,换手操作。
检测与恢复策略:我们可以通过对其进行管理,在发生死锁时,强制让某人或者全部停下,释放锁。
鸵鸟原则:不推荐
换手操作只需要在最后创建哲学家对象时进行修改即可
for (int i = 0; i < philosophers.length; i++) { //解决哲学家问题,换手策略,让一个哲学家,换一个方向拿东西 Object left = chops[i]; Object right = chops[(i+1)%philosophers.length]; if (i == philosophers.length-1){ philosophers[i] = new Philosopher(right,left); }else { philosophers[i] = new Philosopher(left ,right); } new Thread(philosophers[i],"哲学家"+(i+1)+"号").start(); }
那么我们在实际的开发中,如何有效的避免死锁呢?
1.设置超时时间。使用try-lock的方式。
2.多使用并发类,而不自己创建类(ConcurrentHashMap之类的类,原子类等)
3.尽量降低锁的使用粒度,尽可能使用不同的锁而不是同一个锁。
4.尽量使用同步代码块,不适用同步方法。同时使用同步代码块,方便我们控制,自己指定锁
5.新建线程时,要为线程起有意义的名字,方便debug和排查。
6.避免出现锁的嵌套
7.分配资源前能不能收回来:银行家算法
8.尽量不要几个功能使用同一把锁。
活锁(Livelock):
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。但是活锁始终会处于运行状态,甚至比死锁更糟糕。
下面用一个经典的就餐问题,演示活锁的情况。当两人就餐时,只有一个勺子,两个人都十分的谦让,看到对方没有勺子,就会想让给对方。这样就会发生活锁。
package DeadLock; import java.util.Random; /** * @program:多线程和IO * @descripton:牛郎织女演示活锁 * @author:ZhengCheng * @create:2021/10/3-15:59 **/ public class LiveLockDemo { static class Spoon { private Owner owner; public Spoon(Owner owner) { this.owner = owner; } public Owner getOwner() { return owner; } public void setOwner(Owner owner) { this.owner = owner; } public synchronized void use() { System.out.printf("%s吃完了!", owner.name); } } static class Owner { private String name; private boolean isHungry; public Owner(String name) { this.name = name; isHungry = true; } public void eatWith(Spoon spoon, Owner lover) { while (isHungry) { if (spoon.owner != this) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } continue; } if (lover.isHungry /*&& new Random().nextInt(10) < 9*/) { System.out.println(name + ":" + lover.name + "你先吃"); spoon.setOwner(lover); continue; } spoon.use(); isHungry = false; System.out.println(name + "吃完了"); spoon.setOwner(lover); } } } public static void main(String[] args) { Owner o1 = new Owner("a"); Owner o2 = new Owner("b"); Spoon spoon = new Spoon(o1); new Thread(new Runnable() { @Override public void run() { o1.eatWith(spoon,o2); } }).start(); new Thread(new Runnable() { @Override public void run() { o2.eatWith(spoon,o1); } }).start(); } }
上述代码就会发生活锁的情况。两个线程互相谦让,导致没有人开始执行任务。想要解决这个问题,我们最好使用几种方法。比如设计一个谦让的规则,让其不活锁,或者我们可以增加一些随机性,而让其不是百分百谦让。上述的解决方法,只需将代码的注释激活即可。
饥饿:
饥饿 ,与死锁和活锁非常相似。是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。有可能是因为线程的优先级设置得过低,或者有某线程始终持有锁同时有无限循环从而不释放锁,或者某程序始终占用某文件的写锁。
常见面试问题
1.写一个必然死锁的例子。
2.发生死锁必须满足哪些条件(4个) 互斥、请求与保持、不剥夺、循环等待
3.如何定位死锁
4.解决死锁的策略(3个)
5.讲一讲哲学家就餐问题
6.实际工程中如何避免死锁?(8)
7.什么是活跃性问题?活锁、饥饿和死锁有什么区别?