Java多线程之死锁(死锁产生条件、手写简单死锁程序、破坏死锁)(面试常有)
目录
一、死锁。
(1)实际生活"死锁"情景。
- 有这样一个场景:一个中国人和一个美国人在一起吃饭。
- 其中美国人拿了中国人的筷子,中国人拿了美国人的刀叉,于是两个人开始争执不休。
- 中国人:”你先给我筷子,我再给你刀叉!“
- 美国人:“你先给我刀叉,我再给你筷子!”
- .......
(结果可想而知,两个人都吃不成饭...)
- 这个例子中的中国人和美国人相当于不同的"线程",而筷子和刀叉就相当于"锁"。而两个线程在运行的时候都在等待对方的"锁",这样就造成了程序的停滞。
- 这种现象被称为"死锁"。
(2)程序中举例。
- 有两个线程。thread01、thread02。
- thread01:a锁——>b锁。thread02:b锁——>a锁。
- thread01获取完a锁,执行完毕后不释放a锁,接着thread01又去获取b锁。
- 而thread02获取完b锁,执行完毕后不释放b锁,接着thread02有去获取a锁。
- 两个线程同时去执行,就会造成"死锁"情况。
(3)死锁产生必要的条件。
<1> 互斥使用。
- 互斥(排它性)。即当资源被一个线程使用(占有)时,别的线程不能使用。
- 像同步代码块、同步方法一样,拿到"锁"时,其它线程不能再进去访问或使用。
<2> 不可抢占。
- 资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 也就是拿到这个线程后,另外的线程不能抢"锁"。
- 如果各个线程可以"抢锁",就算前面抢到"锁"的线程因为某种原因无法释放"锁",后面等待的线程也可以去抢到"锁",也就不会造成阻塞(死锁)。
<3> 请求和保持。
- 即当资源请求者在请求其他的资源的同时保持对原有资源的占用。
- 如上的两个线程"thread01"、"thread02"。thread01拿到a锁,用完就释放掉,而thread02拿到b锁,用完也释放掉。这样再互相拿自己需要的"锁",就不会造成"死锁"现象。
<4> 循环等待。
- 即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
- 而上面的例子也是一个小等待队列(环)。thread01->a->b->thread02->b->a。
- 如果改成thread01:a锁——>b锁。thread02:a锁——>b锁。也不会造成死锁的情况了。
(注意:上述的四个条件都满足,才会造成"死锁"的产生。)
(4)避免死锁。
- 在实际开发中,为了避免"死锁"的发生,只需要把上述四个条件中的任意一个打破,就可以避免"死锁"的现象产生。
二、IDEA中写简单"死锁程序"。
(也就是整个程序都满足上述的四个条件)
<0>分析与解释。
- "死锁类"不采用实现Runnable接口,重新run()方法。而是在测试中直接使用匿名的Runnable实现子类。()->{}。中括号里面就是实现的run()方法。
- "死锁类"创建的两个类的锁对象(static)。在该"死锁"类的new两个即可。保证a与b"锁"是这个"死锁类"是共享的。
- 测试类中到时候模拟两个线程,同时new两个"死锁"类的对象。d1、d2是不共享的,但是锁a、锁b是共享的资源。
- 测试类中的new Thread(Runnable target)。参数内部是使用Lambda表达式(也可以使用匿名内部类)也就是在重新的run()方法里面进行调用方法操作(d1.执行死锁方法、d2.执行死锁方法)
- 测试类中最后两个线程分别去使用start()方法。然后分别去拿各自的"锁a"、"锁b",然后阻塞(哈哈哈!)
- "死锁类"中这里注意还需要有一个布尔类型的变量。去控制获取锁的顺序。顺序????就是为"true"时控制一个线程拿"锁"时,从a锁——>b锁。值为false时让另外一个线程拿"锁"时,从b锁——>a锁。flag不是设置成共享的(不是static)。
(具体详细代码如下。)
<1>简单死锁程序(类DeadLockDemo)
package com.fs; /** * @Title: DeadLockDemo * @Author HeYouLong * @Package com.fs * @Date 2024/10/12 下午8:41 * @description: 简单死锁程序演示 */ public class DeadLockDemo { //锁a与锁b private static Object a = new Object(); private static Object b = new Object(); //控制拿锁的顺序,a——>b或者b——>a private boolean flag; //提供构造方法改布尔值 public DeadLockDemo(boolean flag) { this.flag = flag; } public void testDeadLock() throws InterruptedException { if(flag){ //a——>b synchronized (a){ System.out.println(Thread.currentThread().getName()+"执行a锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 synchronized (b){ System.out.println(Thread.currentThread().getName()+"执行b锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 } } }else { //b——>a synchronized (b){ System.out.println(Thread.currentThread().getName()+"执行b锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 synchronized (a){ System.out.println(Thread.currentThread().getName()+"执行a锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 } } } } }
<2>测试类(Test)
package com.fs; /** * @Title: Test * @Author HeYouLong * @Package com.fs * @Date 2024/10/13 上午10:59 * @description: 测试类 */ public class Test { public static void main(String[] args) { //创建不同对象 DeadLockDemo d1 = new DeadLockDemo(true); DeadLockDemo d2 = new DeadLockDemo(false); Thread t1 = new Thread(()->{ //线程t1的run()方法使用d1对象 //但是在对象调用testDeadLock()使用的是共享资源的锁 try { d1.testDeadLock(); } catch (InterruptedException e) { throw new RuntimeException(e); } },"线程1"); Thread t2 = new Thread(()->{ //线程t2的run()方法使用d2对象 // 但是在对象调用testDeadLock()使用的是共享资源的锁 try { d2.testDeadLock(); } catch (InterruptedException e) { throw new RuntimeException(e); } },"线程2"); //启动线程 t1.start(); t2.start(); } }
<3>程序执行结果。(发生"死锁")
<4>破坏上面的"简单死锁程序"。
(I)破坏互斥条件。
(II)破坏不可抢占条件。
(III)破坏请求和保持条件。
- 让两个线程"t1"、"t2"都执行完资源后释放完各自的"锁",然后再去申请其它"锁"。(不允许它保持)
public void testDeadLock() throws InterruptedException { if(flag){ //a——>b synchronized (a){ System.out.println(Thread.currentThread().getName()+"执行a锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 } synchronized (b){ System.out.println(Thread.currentThread().getName()+"执行b锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 } }else { //b——>a synchronized (b){ System.out.println(Thread.currentThread().getName()+"执行b锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 } synchronized (a){ System.out.println(Thread.currentThread().getName()+"执行a锁代码块的资源"); Thread.sleep(100); //模拟执行完后等待 } } }
(IIII)破坏循环等待条件。
- 简单修改即可。不让他们请求锁时形成环路。(把顺序都变成"锁a"——>"锁b")
三、总结
(1)关于死锁。
- "死锁"现象造成,必须四个条件都满足了。只要破坏其中一个条件,"死锁"的现象就不存在了。
- 以后写代码,尽量让加锁的顺序保持一致。或者某个线程操作多个锁,操作完成之后,要让它释放掉。
- 在实际开发中,只需要把上述四个条件任意一个打破,就可以最大可能的避免死锁。
原文地址:https://blog.csdn.net/m0_74363339/article/details/142885031
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!