死锁的3种死法
1. 什么是死锁
在多线程环境中,多个进程可以竞争有限数量的资源。当一个进程申请资源时,如果这时没有可用资源,那么这个进程进入等待状态。有时,如果所申请的资源被其他等待进程占有,那么该等待进程有可能再也无法改变状态。这种情况称为死锁
在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再往下执行。我们只能通过中止并重启的方式来让程序重新执行。
2. 造成死锁的原因
- 当前线程拥有其他线程需要的资源
- 当前线程等待其他线程已拥有的资源
- 都不放弃自己拥有的资源
3. 死锁的必要条件
3.1 互斥
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
3.2 不可剥夺
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
3.3 请求与保持
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
3.4 循环等待
是指进程发生死锁后,必然存在一个进程–资源之间的环形链,通俗讲就是你等我的资源,我等你的资源,大家一直等。
4. 死锁的分类
4.1 静态顺序型死锁
线程之间形成相互等待资源的环时,就会形成顺序死锁lock-ordering deadlock,多个线程试图以不同的顺序来获取相同的锁时,容易形成顺序死锁,如果所有线程以固定的顺序来获取锁,就不会出现顺序死锁问题
经典案例是LeftRightDeadlock,两个方法,分别是leftRigth、rightLeft。如果一个线程调用leftRight,另一个线程调用rightLeft,且两个线程是交替执行的,就会发生死锁。
public class LeftRightDeadLock { //左边锁 private static Object left = new Object(); //右边锁 private static Object right = new Object(); /** * 现持有左边的锁,然后获取右边的锁 */ public static void leftRigth() { synchronized (left) { System.out.println("leftRigth: left lock,threadId:" + Thread.currentThread().getId()); //休眠增加死锁产生的概率 sleep(100); synchronized (right) { System.out.println("leftRigth: right lock,threadId:" + Thread.currentThread().getId()); } } } /** * 现持有右边的锁,然后获取左边的锁 */ public static void rightLeft() { synchronized (right) { System.out.println("rightLeft: right lock,threadId:" + Thread.currentThread().getId()); //休眠增加死锁产生的概率 sleep(100); synchronized (left) { System.out.println("rightLeft: left lock,threadId:" + Thread.currentThread().getId()); } } } /** * 休眠 * * @param time */ private static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { //创建一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); executorService.execute(() -> leftRigth()); executorService.execute(() -> rightLeft()); executorService.shutdown(); } }
输出:
leftRigth: left lock,threadId:12 rightLeft: right lock,threadId:13
我们发现,12号线程锁住了左边要向右边获取锁,13号锁住了右边,要向左边获取锁,因为两边都不释放自己的锁,互不相让,就产生了死锁。
4.1.1 解决方案
固定加锁的顺序(针对锁顺序死锁)
只要交换下锁的顺序,让线程来了之后先获取同一把锁,获取不到就等待,等待上一个线程释放锁再获取锁。
public static void leftRigth() { synchronized (left) { ... synchronized (right) { ... } } } public static void rightLeft() { synchronized (left) { ... synchronized (right) { ... } } }
4.2 动态锁顺序型死锁
由于方法入参由外部传递而来,方法内部虽然对两个参数按照固定顺序进行加锁,但是由于外部传递时顺序的不可控,而产生锁顺序造成的死锁,即动态锁顺序死锁。
上例告诉我们,交替的获取锁会导致死锁,且锁是固定的。有时候锁的执行顺序并不那么清晰,参数导致不同的执行顺序。经典案例是银行账户转账,from账户向to账户转账,在转账之前先获取两个账户的锁,然后开始转账,如果这是to账户向from账户转账,角色互换,也会导致锁顺序死锁。
/** * 动态顺序型死锁 * 转账业务 */ public class TransferMoneyDeadlock { public static void transfer(Account from, Account to, int amount) { //先锁住转账的账户 synchronized (from) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功"); //休眠增加死锁产生的概率 sleep(100); //在锁住目标账户 synchronized (to) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功"); if (from.balance < amount) { System.out.println("余额不足"); return; } else { from.debit(amount); to.credit(amount); System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功"); } } } } private static class Account { String name; int balance; public Account(String name, int balance) { this.name = name; this.balance = balance; } void debit(int amount) { this.balance = balance - amount; } void credit(int amount) { this.balance = balance + amount; } } /** * 休眠 * * @param time */ private static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { //创建线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); //创建账户A Account A = new Account("A", 100); //创建账户B Account B = new Account("B", 200); //A -> B 的转账 executorService.execute(() -> transfer(A, B, 5)); //B -> A 的转账 executorService.execute(() -> transfer(B, A, 10)); executorService.shutdown(); } }
输出:
线程【12】获取【A】账户锁成功 线程【13】获取【B】账户锁成功
然后就没有然后了,产生了死锁,我们发现 因为对象的调用关系,产生了互相锁住资源的问题。
4.2.1 解决方案
根据传入对象的hashCode硬性确定加锁顺序,消除可变性,避免死锁。
package com.test.thread.deadlock; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 动态顺序型死锁解决方案 */ public class TransferMoneyDeadlock { /** * 监视器,第三把锁,为了方式HASH冲突 */ private static Object lock = new Object(); /** * 我们经过上一次得失败,明白了不能依赖参数名称简单的确定锁的顺序,因为参数是 * 具有动态性的,所以,我们改变一下思路,直接根据传入对象的hashCode()大小来 * 对锁定顺序进行排序(这里要明白的是如何排序不是关键,有序才是关键)。 * * @param from * @param to * @param amount */ public static void transfer(Account from, Account to, int amount) { /** * 这里需要说明一下为什么不使用HashCode()因为HashCode方法可以被重写, * 所以,我们无法简单的使用父类或者当前类提供的简单的hashCode()方法, * 所以,我们就使用系统提供的identityHashCode()方法,该方法保证无论 * 你是否重写了hashCode方法,都会在虚拟机层面上调用一个名为JVM_IHashCode * 的方法来根据对象的存储地址来获取该对象的hashCode(),HashCode如果不重写 * 的话,其实也是通过这个虚拟机层面上的方法,JVM_IHashCode()方法实现的 * 这个方法是用C++实现的。 */ int fromHash = System.identityHashCode(from); int toHash = System.identityHashCode(to); if (fromHash > toHash) { //先锁住转账的账户 synchronized (from) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功"); //休眠增加死锁产生的概率 sleep(100); //在锁住目标账户 synchronized (to) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功"); if (from.balance < amount) { System.out.println("余额不足"); return; } else { from.debit(amount); to.credit(amount); System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功"); } } } } else if (fromHash < toHash) { //先锁住转账的账户 synchronized (to) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功"); //休眠增加死锁产生的概率 sleep(100); //在锁住目标账户 synchronized (from) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功"); if (from.balance < amount) { System.out.println("余额不足"); return; } else { from.debit(amount); to.credit(amount); System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功"); } } } } else { //如果传入对象的Hash值相同,那就加让加第三层锁 synchronized (lock) { //先锁住转账的账户 synchronized (from) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功"); //休眠增加死锁产生的概率 sleep(100); //在锁住目标账户 synchronized (to) { System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功"); if (from.balance < amount) { System.out.println("余额不足"); return; } else { from.debit(amount); to.credit(amount); System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功"); } } } } } } private static class Account { String name; int balance; public Account(String name, int balance) { this.name = name; this.balance = balance; } void debit(int amount) { this.balance = balance - amount; } void credit(int amount) { this.balance = balance + amount; } } /** * 休眠 * * @param time */ private static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { //创建线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); //创建账户A Account A = new Account("A", 100); //创建账户B Account B = new Account("B", 200); //A -> B 的转账 executorService.execute(() -> transfer(A, B, 5)); //B -> A 的转账 executorService.execute(() -> transfer(B, A, 10)); executorService.shutdown(); } }
输出
线程【12】获取【A】账户锁成功 线程【12】获取【B】账户锁成功 线程【12】从【A】账户转账到【B】账户【5】元钱成功 线程【13】获取【B】账户锁成功 线程【13】获取【A】账户锁成功 线程【13】从【B】账户转账到【A】账户【10】元钱成功
4.3 协作对象间的死锁
在协作对象之间可能存在多个锁获取的情况,但是这些获取多个锁的操作并不像在LeftRightDeadLock或transferMoney中那么明显,这两个锁并不一定必须在同一个方法中被获取。如果在持有锁时调用某个外部方法,那么这就需要警惕死锁问题,因为在这个外部方法中可能会获取其他锁,或者阻塞时间过长,导致其他线程无法及时获取当前被持有的锁。
上述两例中,在同一个方法中获取两个锁。实际上,锁并不一定在同一方法中被获取。经典案例,如出租车调度系统。
/** * 协作对象间的死锁 */ public class CoordinateDeadlock { /** * Taxi 类 */ static class Taxi { private String location; private String destination; private Dispatcher dispatcher; public Taxi(Dispatcher dispatcher, String destination) { this.dispatcher = dispatcher; this.destination = destination; } public synchronized String getLocation() { return this.location; } /** * 该方法先获取Taxi的this对象锁后,然后调用Dispatcher类的方法时,又需要获取 * Dispatcher类的this方法。 * * @param location */ public synchronized void setLocation(String location) { this.location = location; System.out.println(Thread.currentThread().getName() + " taxi set location:" + location); if (this.location.equals(destination)) { dispatcher.notifyAvailable(this); } } } /** * 调度类 */ static class Dispatcher { private Set<Taxi> taxis; private Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<Taxi>(); availableTaxis = new HashSet<Taxi>(); } public synchronized void notifyAvailable(Taxi taxi) { System.out.println(Thread.currentThread().getName() + " notifyAvailable."); availableTaxis.add(taxi); } /** * 打印当前位置:有死锁风险 * 持有当前锁的时候,同时调用Taxi的getLocation这个外部方法;而这个外部方法也是需要加锁的 * reportLocation的锁的顺序与Taxi的setLocation锁的顺序完全相反 */ public synchronized void reportLocation() { System.out.println(Thread.currentThread().getName() + " report location."); for (Taxi t : taxis) { t.getLocation(); } } public void addTaxi(Taxi taxi) { taxis.add(taxi); } } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); final Dispatcher dispatcher = new Dispatcher(); final Taxi taxi = new Taxi(dispatcher, "软件园"); dispatcher.addTaxi(taxi); //先获取dispatcher锁,然后是taxi的锁 executorService.execute(() -> dispatcher.reportLocation()); //先获取taxi锁,然后是dispatcher的锁 executorService.execute(() -> taxi.setLocation("软件园")); executorService.shutdown(); } }
4.3.1 解决方案
使用开放调用,开放调用指调用该方法不需要持有锁。
开放调用,是指在调用某个方法时不需要持有锁。开放调用可以避免死锁,这种代码更容易编写。上述调度算法完全可以修改为开发调用,修改同步代码块的范围,使其仅用于保护那些涉及共享状态的操作,避免在同步代码块中执行方法调用。修改Dispatcher的reportLocation方法:
4.3.1.1 setLocation方法
/** * 开放调用,不持有锁期间进行外部方法调用 * * @param location */ public void setLocation(String location) { synchronized (this) { this.location = location; } System.out.println(Thread.currentThread().getName() + " taxi set location:" + location); if (this.location.equals(destination)) { dispatcher.notifyAvailable(this); } }
4.3.1.2 reportLocation 方法
/** * 同步块只包含对共享状态的操作代码 */ public synchronized void reportLocation() { System.out.println(Thread.currentThread().getName() + " report location."); Set<Taxi> taxisCopy; synchronized (this) { taxisCopy = new HashSet<Taxi>(taxis); } for (Taxi t : taxisCopy) { t.getLocation(); } }
本文由
传智教育博学谷
教研团队发布。如果本文对您有帮助,欢迎
关注
和点赞
;如果您有任何建议也可留言评论
或私信
,您的支持是我坚持创作的动力。转载请注明出处!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
官宣!Taier1.3新版本正式发布,新鲜功能抢先体验
2022年11月7日,Taier1.3版本正式发布! Taier 是一个大数据分布式可视化的DAG任务调度系统,旨在降低ETL开发成本、提高大数据平台稳定性,大数据开发人员可以在 Taier 直接进行业务逻辑的开发,而不用关心任务错综复杂的依赖关系与底层的大数据平台的架构实现,将工作的重心更多地聚焦在业务之中。 Taier自今年2月份开源之后,得到了社区开发者的广泛支持,我们积极吸收社区开发者的意见建议,不断迭代版本,目前已发布了Taier1.1、Taier1.2这两个大的版本更新。 本次发布的Taier1.3版本,我们新增了Flink Standalone组件,融合了DataSourceX的模块,并新增了Python、Shell、ClickHouse、Doris SQL等多种任务,任务数据源绑定,任务支持指定队列运行,并进行了部署优化,同时对官网社区的UI进行了全新的升级改造。 目前新版本已在Github与Gitee上线,同时使用文档也在社区推送,大家可以随时下载查阅,欢迎大家前往体验(喜欢我们的项目欢迎大家点个Star),体验地址: Github: https://github.c...
- 下一篇
python 小学数学口算题卷子自动生成发布全新版本
Primary School Mathematics 小学数学口算题 孩子上小学一年级了,加减乘除的口算就要开始练习了,估计老师肯定会让家长出题,所以提前准备一下,利用Python开发了一套自动生成小学生口算题的小应用。 为了让辛苦的程序员老爹解放抄题的双手,本程序让你拥有更多的时间去写代码而不用去手写几道口算题而劳神伤脑。所以有或没有娃子的程序员老爹们一起来继续优化个开源小程序的?有什么点子,发现什么BUG,欢迎提出issue。 仅以此软件,献给那些热爱Python的程序员老爹们! 程序更新 2022-11-05 由于之前代码所用的框架是比较久远的版本,很多新人需要折腾很长时间才能运行起来,鉴于此种情况,全面更换了前后端的框架,使程序的运行环境搭建更为方便了.重构期间修复了一些bug和逻辑,后续将更新一些新功能.比如一键生成预制的常见类型的口算题等. 程序核心功能: 1.可以设置各算数项和结果的取值范围及多步算数符号的选择,可以生成求结果、求算数项、带括号的算式,最多支持3步算式题,除法可以生成带有余数的口算题. 2.可以简单设置文档标题,小标题。设置生成的口算题文档个数 3.一键...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境