一不小心就死锁了,怎么办?

在上一篇文章中,我们用 Account.class 作为互斥锁,来解决银行业务里面的转账问题,虽然这个方案不存在并发问题,但是所有账户的转账操作都是串行的,性能太差。

向现实世界要答案

我们试想在古代,没有信息化,账户的存在形式真的就是一个账本,而且每个账户都有一个账本,这些账本都统一存放在文件架上。银行柜员在给我们做转账时,要去文件架上把转出账本和转入账本都拿到手,然后做转账。这个柜员在拿账本的时候可能遇到以下三种情况:

文件架上恰好有转出账本和转入账本,那就同时拿走;

如果文件架上只有转出账本和转入账本之一,那这个柜员就先把文件架上有的账本拿到手,同时等着其他柜员把另外一个账本送回来;

转出账本和转入账本都没有,那这个柜员就等着两个账本都被送回来。

此处王老师应该只是想给我们构建一个场景,特地查了下,参考此处 简单说下,我国古代奴隶社会时,一个人管一种账号,“司书掌管会计账簿,职内掌管财务收入账户,职岁掌管财务支出类账户,职币掌管财务结余”,称““单式记账法””,后又有“入出记账法”。

上面这个过程在编程的世界里怎么实现呢?其实用两把锁就实现了,转出账本一把,转入账本另一把。在 transfer() 方法内部,我们首先尝试锁定转出账户 this(先把转出账本拿到手),然后尝试锁定转入账户 target(再把转入账本拿到手),只有当两者都成功时,才执行转账操作。这个逻辑可以图形化为下图这个样子。

1 classAccount {2 private intbalance;3 //转账

4 void transfer(Account target, intamt){5 //锁定转出账户

6 synchronized(this) {7 //锁定转入账户

8 synchronized(target) {9 if (this.balance >amt) {10 this.balance -=amt;11 target.balance +=amt;12 }13 }14 }15 }16 }

上面的实现看上去很完美,并且也算是将锁用得出神入化了。相对于用 Account.class 作为互斥锁,锁定的范围太大,而我们锁定两个账户范围就小多了,这样的锁,上一章我们介绍过,叫细粒度锁。

使用细粒度锁可以提高并行度,是性能优化的一个重要手段。

使用细粒度锁这么简单,有这样的好事,是不是也要付出点什么代价啊?

的确,使用细粒度锁是有代价的,这个代价就是可能会导致死锁。

死锁的一个比较专业的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。如下图

如何预防死锁

并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题最好的办法还是规避死锁。

如何避免死锁呢?

要避免死锁就需要分析死锁发生的条件,有个叫 Coffman 的牛人早就总结过了,只有以下这四个条件都发生时才会出现死锁:

互斥,共享资源 X 和 Y 只能被一个线程占用;

占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;

不可抢占,其他线程不能强行抢占线程 T1 占有的资源;

循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

只要我们破坏其中一个,就可以成功避免死锁的发生。

互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。

对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。

对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

那具体如何体现在代码上呢?

破坏 占用且等待 条件

从理论上讲,要破坏这个条件,可以一次性申请所有资源。在现实世界里,就拿前面我们提到的转账操作来讲,它需要的资源有两个,一个是转出账户,另一个是转入账户,当这两个账户同时被申请时,我们该怎么解决这个问题呢?

可以增加一个账本管理员,然后只允许账本管理员从文件架上拿账本,也就是说柜员不能直接在文件架上拿账本,必须通过账本管理员才能拿到想要的账本。这样就保证了“一次性申请所有资源”。(解决不了的问题就再加一个中间层?)

“同时申请”这个操作是一个临界区,我们也需要一个角色(Java 里面的类)来管理这个临界区,我们就把这个角色定为 Allocator。它有两个重要功能,分别是:同时申请资源 apply() 和同时释放资源 free()。

账户 Account 类里面持有一个 Allocator 的单例(必须是单例,只能由一个人来分配资源)。

当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知 Allocator 同时释放转出账户和转入账户这两个资源。

具体的代码实现如下:

1 classAllocator {2 private List als = new ArrayList<>();3 //一次性申请所有资源

4 synchronized booleanapply(5 Object from, Object to){6 if(als.contains(from) ||

7 als.contains(to)){8 return false;9 } else{10 als.add(from);11 als.add(to);12 }13 return true;14 }15 //归还资源

16 synchronized voidfree(17 Object from, Object to){18 als.remove(from);19 als.remove(to);20 }21 }22

23 classAccount {24 //actr应该为单例 //这个单例怎么实现?

25 privateAllocator actr;26 private intbalance;27 //转账

28 void transfer(Account target, intamt){29 //一次性申请转出账户和转入账户,直到成功

30 while(!actr.apply(this, target)) // 原理类似CAS,也是自旋,实际项目中需要加入超时时间,避免一直阻塞31 ;32 try{33 //锁定转出账户

34 synchronized(this){35 //锁定转入账户

36 synchronized(target){37 if (this.balance >amt){38 this.balance -=amt;39 target.balance +=amt;40 }41 }42 }43 } finally{44 actr.free(this, target)45 }46 }47 }

破坏 不可抢占 条件

破坏不可抢占条件看上去很简单,核心是要能够主动释放它占有的资源,这一点 synchronized 是做不到的。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。

Java 在语言层次确实没有解决这个问题,不过在 SDK 层面还是解决了的,java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。

简单说一下synchronized的原理?

破坏 循环等待 条件

破坏这个条件,需要对资源进行排序,然后按序申请资源。

我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请(大到小也行,重点是排序)。

1 classAccount {2 private intid;3 private intbalance;4 //转账

5 void transfer(Account target, intamt){6 Account left = this;①7 Account right =target; ②8 if (this.id >target.id) { ③9 left =target; ④10 right = this; ⑤11 } ⑥12 //锁定序号小的账户

13 synchronized(left){14 //锁定序号大的账户

15 synchronized(right){16 if (this.balance >amt){17 this.balance -=amt;18 target.balance +=amt;19 }20 }21 }22 }23 }

总结

当我们在编程世界里遇到问题时,应不局限于当下,可以换个思路,向现实世界要答案,利用现实世界的模型来构思解决方案,这样往往能够让我们的方案更容易理解,也更能够看清楚问题的本质。

识别出风险很重要。

识别出风险很重要。

我们在选择具体方案的时候,还需要评估一下操作成本,从中选择一个成本最低的方案。

课后思考

我们上面提到:破坏占用且等待条件,我们也是锁了所有的账户,而且还是用了死循环 while(!actr.apply(this, target));这个方法,那它比 synchronized(Account.class) 有没有性能优势呢?

引自极客用户:

虽然上面两种锁的方式都是串行化了,但是具体还是有一点区别的:synchronized(Account.class)的方式相当于A->B 转账,C->D转账 先后执行,而 actr.apply(this, target)的方式则是apply-->转账-->free这样的串行方式执行,但是在转账中是可以A->B,C->D转账线程并行执行的,正如文中提到的apply方法耗时很少 所以比如一次转账耗时200ms,apply+release方式执行要20ms,所以用synchronized的方式A->B,C->D则需要耗时400ms,而appy的方式则要200+20*2=240ms,并且同时转账的人越多 apply方式的转账并行度越高 比synchronized的方式的优势越明显。

对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

只要我们破坏其中一个,就可以成功避免死锁的发生。

能太差。

性能太差。

java判断一个月连续打卡时间_java并发编程实战《五》死锁 挑战打卡60天相关推荐

  1. c++并发编程实战_Java 并发编程实战:JAVA中断线程几种基本方法

    一个多线程Java程序,只有当其全部线程执行结束时(更具体地说,是所有非守护线程结束或者某个线程调用system.exit()方法的时候) ,才会结束运行.有时,为了终止程序或者取消一个线程对象所执行 ...

  2. java的尝试性问题_Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和有序性的问题,那么还有一个原子性问题咱们还没解决.在第一篇文章01并发编程的Bug源头当中,讲到了把一个或者多 ...

  3. java 如何知道对象是否被修改过_Java 并发编程:AQS 的原子性如何保证

    当我们研究AQS框架时(对于AQS不太熟知可以先阅读<什么是JDK内置并发框架AQS>,会发现AbstractQueuedSynchronizer这个类很多地方都使用了CAS操作.在并发实 ...

  4. java判断一个月有多少天代码_java oracle 查询一个月有多少天

    查询月的天数 java Calendar c= Calendar.getInstance(); c.set(Calendar.YEAR, nYear); c.set(Calendar.MONTH, n ...

  5. java判断一个月间隔_如何检查间隔列表(Joda-Time)是否完全涵盖Java中的一个月

    您可以使用下一个方法 static boolean covers(Interval month, List intervals) { //assumes intervals are sorted al ...

  6. java 并发测试程序_java并发编程实战:第十二章---并发程序的测试

    并发程序中潜在错误的发生并不具有确定性,而是随机的. 安全性测试:通常会采用测试不变性条件的形式,即判断某个类的行为是否与其规范保持一致 活跃性测试:进展测试和无进展测试两方面,这些都是很难量化的(性 ...

  7. java并发编程 目录_Java并发编程实战的作品目录

    展开全部 对本书的赞誉 译者序 前 言 第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力e5a48de588b662616964757a686964616f313 ...

  8. Java并发编程实战之互斥锁

    文章目录 Java并发编程实战之互斥锁 如何解决原子性问题? 锁模型 Java synchronized 关键字 Java synchronized 关键字 只能解决原子性问题? 如何正确使用Java ...

  9. java判断一个日期是否为工作日

    java判断一个日期是否为工作日 /*** @Author :feiyang* @Date :Created in 7:47 PM 2019/12/3*/ public class LocalDate ...

最新文章

  1. Linux下Flash-LED的处理
  2. mysql处理字符串的两个绝招:substring_index,concat
  3. PyTorch学习笔记(二)——回归
  4. PHP获取重定向URL的几种方法
  5. 成功解决RuntimeError: Java is not installed, or the Java executable is not on system path
  6. 模块说和神经网络学说_让神经网络解释自己:牛津大学博士小姐姐,用毕业论文揭示“炼丹炉”结构...
  7. 详解:Spark程序的开始 SparkContext 源码走一走
  8. 菜鸟入门:电脑常用的9个小知识点
  9. Mybatis框架Mybatis下载步骤
  10. web中常见的敏感信息
  11. voyage java_Voyage:Java 实现的基于 Netty 的轻量、高性能分布式 RPC 服务框架
  12. Halcon 毛刺检测
  13. 在购买太阳眼镜时怎样辨别好坏
  14. 专升本英语——语法知识——高频语法——第六节 名词性从句(主语从句-表语从句-同位语从句-宾语从句)【学习笔记】
  15. Hadoop3.x学习教程(二)
  16. windows程序设计(一)
  17. 10 行代码,集算器实现写诗机器人
  18. python关于类的通俗描述?
  19. python数据分析实战五_简单的python数据分析实战——黑五销售数据分析
  20. python入门教程NO.1 用python打印你的宠物小精灵吧

热门文章

  1. 招投标基本流程(不包含资格预审)
  2. 计算机网络第一章总结
  3. (转)FFmpeg源代码简单分析:avformat_open_input()
  4. python带你制作一个gequ下载器,海量gequ免费听
  5. php购物车程序,PHP购物车程序设计
  6. windows系统怎么打开自带摄像头?(然并卵的回答)
  7. 说下更新百度快照的利弊
  8. 365地图java_数字说话 中国最全的电子地图网站
  9. 如何将压缩包变成一张图片
  10. STM32F4DISCOVERY rtthread-3.1.5入门指南