05 | 一不小心就死锁了,怎么办?
王宝令
该思维导图由 AI 生成,仅供参考
在上一篇文章中,我们用 Account.class 作为互斥锁,来解决银行业务里面的转账问题,虽然这个方案不存在并发问题,但是所有账户的转账操作都是串行的,例如账户 A 转账户 B、账户 C 转账户 D 这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了,这样的话,性能太差。
试想互联网支付盛行的当下,8 亿网民每人每天一笔交易,每天就是 8 亿笔交易;每笔交易都对应着一次转账操作,8 亿笔交易就是 8 亿次转账操作,也就是说平均到每秒就是近 1 万次转账操作,若所有的转账操作都串行,性能完全不能接受。
那下面我们就尝试着把性能提升一下。
向现实世界要答案
现实世界里,账户转账操作是支持并发的,而且绝对是真正的并行,银行所有的窗口都可以做转账操作。只要我们能仿照现实世界做转账操作,串行的问题就解决了。
我们试想在古代,没有信息化,账户的存在形式真的就是一个账本,而且每个账户都有一个账本,这些账本都统一存放在文件架上。银行柜员在给我们做转账时,要去文件架上把转出账本和转入账本都拿到手,然后做转账。这个柜员在拿账本的时候可能遇到以下三种情况:
文件架上恰好有转出账本和转入账本,那就同时拿走;
如果文件架上只有转出账本和转入账本之一,那这个柜员就先把文件架上有的账本拿到手,同时等着其他柜员把另外一个账本送回来;
转出账本和转入账本都没有,那这个柜员就等着两个账本都被送回来。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
本文介绍了并发编程中可能出现的死锁问题以及如何避免死锁的解决方案。通过银行转账业务中使用细粒度锁来提高并行度的方法引出死锁问题,并分析了死锁发生的条件。作者提出了避免死锁的方法,包括一次性申请所有资源、主动释放已占有的资源以及按序申请资源来预防循环等待。文章强调了将理论应用到实践的意愿,但未给出具体的代码实现。总体而言,通过生动的例子和清晰的逻辑,向读者介绍了死锁问题及其解决方案。文章还提到了破坏占用且等待条件、破坏不可抢占条件和破坏循环等待条件的具体方法,以及在编程世界中遇到问题时应该如何换个思路,向现实世界寻求答案。最后,文章强调了识别风险的重要性,并提出了课后思考的问题,引发读者深入思考。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》,新⼈⾸单¥59
《Java 并发编程实战》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(239)
- 最新
- 精选
- 捞鱼的搬砖奇synchronized(Account.class) 锁了Account类相关的所有操作。相当于文中说的包场了,只要与Account有关联,通通需要等待当前线程操作完成。while死循环的方式只锁定了当前操作的两个相关的对象。两种影响到的范围不同。
作者回复: 还真是这样啊!
2019-03-0917158 - Tony Duwhile循环是不是应该有个timeout,避免一直阻塞下去?
作者回复: 你考虑的很周到!👍 加超时在实际项目中非常重要!
2019-03-095122 - 张立华之前遇到死锁,我就是用资源id的从小到大的顺序去申请锁解决的
作者回复: 这个方案最简单
2019-03-127109 - Demon.Leewhile(actr.apply(this, target)); --> while(!actr.apply(this, target)); 我感觉应该是这样,老师,我理解错了?
作者回复: 你发现了个大bug!感谢感谢!!!我这就修改一下啊
2019-03-09580 - 几字凉了秋丶老师,请问一下,在实际的开发中,account对象应该是从数据库中查询出来的吧,假如A转B,C转B一起执行,那B的account对象如何保证是同一个对象,不太理解。。。
作者回复: 实际开发中都是用数据库事务+乐观锁的方式解决的。这个就是个例子,为了说明死锁是怎么回事,以及死锁问题怎么解决。
2019-03-10462 - 别皱眉@阿官 我来回答下你的问题 以下是阿官的问题 ------------------------------------------------------- 老师,在破坏占用且等待的案例中,为何申请完两个账户的资源后还需要再分别锁定this和target账户呢? ------------------------------------------------------- 因为还存在其他业务啊 比如客户取款 这个时候也是对全局变量balance做操作 如果不加锁 并发情况下会出问题 老师你看我说的对吗😄😄
作者回复: 你说到我心里了😃😃😃
2019-03-141648 - 李可威老师为什么按序申请资源就可以破坏循环等待条件呢?这点没有看懂求解答
作者回复: 循环等待,一定是A->B->C->...->N->A形成环状。 如果按需申请,是不允许N->A出现的,只能N->P。没有环状,也就不会死锁了。
2019-03-17839 - Bright丶老师,感觉下面的代码也能避免死锁,并且能实现功能: void transfer(Account target, int amt){ boolean isTransfer = false; // 锁定转出账户 synchronized(this){ if (this.balance > amt) { this.balance -= amt; isTransfer = true; } if (!isTransfer) { return; } // 锁定转入账户 synchronized(target){ target.balance += amt; } } 反映到现实中的场景:服务员A拿到账本1先判断余额够不够,够的话先扣款,再等待其他人操作完账本2,才增加它的额度。 但是这样转账和到账就存在一个时差,现实生活中也是这样,转账不会立马到账,短信提醒24小时内到账,所谓的最终一致性。 老师帮忙看看这样实现会不会有啥其他问题?
作者回复: 实际工作中也有这么做的,只不过是把转入操作放到mq里面,mq消费失败会重试,所以能保证最终一致性。
2019-04-262032 - 轻歌赋存在性能差距,虽然申请的时候加锁导致单线程访问,但是hash判断和赋值时间复杂度低,而在锁中执行业务代码时间长很多。 申请的时候单线程,但是执行的时候就可以多线程了,这里性能提升比较明显 想问问老师,如何判断多线程的阻塞导致的问题呢?有什么工具吗
作者回复: 可以用top命令查看Java线程的cpu利用率,用jstack来dump线程。开发环境可以用 java visualvm查看线程执行情况
2019-03-0927 - 王二宝最常见的就是B转A的同时,A转账给B,那么先锁B再锁A,但是,另一个线程是先锁A再锁B,然而,如果两个线程同时执行,那么就是出现死锁的情况,线程T1锁了A请求锁B,此时线程T2锁了B请求锁A,都在等着对方释放锁,然而自己都不会释放锁,故死锁。 最简单的办法,就是无论哪个线程执行的时候,都按照顺序加锁,即按照A和B的id大小来加锁,这样,无论哪个线程执行的时候,都会先加锁A,再加锁B,A被加锁,则等待释放。这样就不会被死锁了。
作者回复: 👍
2019-08-28221
收起评论