Java并发编程实战
王宝令
资深架构师
立即订阅
15034 人已学习
课程目录
已完结 50 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 你为什么需要学习并发编程?
免费
学习攻略 (1讲)
学习攻略 | 如何才能学好并发编程?
第一部分:并发理论基础 (13讲)
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
第二部分:并发工具类 (14讲)
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
第三部分:并发设计模式 (10讲)
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
第四部分:案例分析 (4讲)
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
第五部分:其他并发模型 (4讲)
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
结束语 (1讲)
结束语 | 十年之后,初心依旧
用户故事 (2讲)
用户来信 | 真好,面试考到这些并发编程,我都答对了!
3 个用户来信 | 打开一个新的并发世界
Java并发编程实战
登录|注册

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

王宝令 2019-03-09
在上一篇文章中,我们用 Account.class 作为互斥锁,来解决银行业务里面的转账问题,虽然这个方案不存在并发问题,但是所有账户的转账操作都是串行的,例如账户 A 转账户 B、账户 C 转账户 D 这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了,这样的话,性能太差。
试想互联网支付盛行的当下,8 亿网民每人每天一笔交易,每天就是 8 亿笔交易;每笔交易都对应着一次转账操作,8 亿笔交易就是 8 亿次转账操作,也就是说平均到每秒就是近 1 万次转账操作,若所有的转账操作都串行,性能完全不能接受。
那下面我们就尝试着把性能提升一下。

向现实世界要答案

现实世界里,账户转账操作是支持并发的,而且绝对是真正的并行,银行所有的窗口都可以做转账操作。只要我们能仿照现实世界做转账操作,串行的问题就解决了。
我们试想在古代,没有信息化,账户的存在形式真的就是一个账本,而且每个账户都有一个账本,这些账本都统一存放在文件架上。银行柜员在给我们做转账时,要去文件架上把转出账本和转入账本都拿到手,然后做转账。这个柜员在拿账本的时候可能遇到以下三种情况:
文件架上恰好有转出账本和转入账本,那就同时拿走;
如果文件架上只有转出账本和转入账本之一,那这个柜员就先把文件架上有的账本拿到手,同时等着其他柜员把另外一个账本送回来;
转出账本和转入账本都没有,那这个柜员就等着两个账本都被送回来。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(144)

  • 捞鱼的搬砖奇
    synchronized(Account.class) 锁了Account类相关的所有操作。相当于文中说的包场了,只要与Account有关联,通通需要等待当前线程操作完成。while死循环的方式只锁定了当前操作的两个相关的对象。两种影响到的范围不同。

    作者回复: 还真是这样啊!

    2019-03-09
    3
    48
  • Tony Du
    while循环是不是应该有个timeout,避免一直阻塞下去?

    作者回复: 你考虑的很周到!👍
    加超时在实际项目中非常重要!

    2019-03-09
    2
    39
  • Demon.Lee
    while(actr.apply(this, target)); --> while(!actr.apply(this, target));
    我感觉应该是这样,老师,我理解错了?

    作者回复: 你发现了个大bug!感谢感谢!!!我这就修改一下啊

    2019-03-09
    34
  • 张立华
    之前遇到死锁,我就是用资源id的从小到大的顺序去申请锁解决的

    作者回复: 这个方案最简单

    2019-03-12
    1
    23
  • 几字凉了秋丶
    老师,请问一下,在实际的开发中,account对象应该是从数据库中查询出来的吧,假如A转B,C转B一起执行,那B的account对象如何保证是同一个对象,不太理解。。。

    作者回复: 实际开发中都是用数据库事务+乐观锁的方式解决的。这个就是个例子,为了说明死锁是怎么回事,以及死锁问题怎么解决。

    2019-03-10
    18
  • 别皱眉
    @阿官 我来回答下你的问题

    以下是阿官的问题
    -------------------------------------------------------
    老师,在破坏占用且等待的案例中,为何申请完两个账户的资源后还需要再分别锁定this和target账户呢?
    -------------------------------------------------------
    因为还存在其他业务啊 比如客户取款
    这个时候也是对全局变量balance做操作
    如果不加锁 并发情况下会出问题

    老师你看我说的对吗😄😄

    作者回复: 你说到我心里了😃😃😃

    2019-03-14
    2
    14
  • 轻歌赋
    存在性能差距,虽然申请的时候加锁导致单线程访问,但是hash判断和赋值时间复杂度低,而在锁中执行业务代码时间长很多。
    申请的时候单线程,但是执行的时候就可以多线程了,这里性能提升比较明显

    想问问老师,如何判断多线程的阻塞导致的问题呢?有什么工具吗

    作者回复: 可以用top命令查看Java线程的cpu利用率,用jstack来dump线程。开发环境可以用 java visualvm查看线程执行情况

    2019-03-09
    12
  • 邋遢的流浪剑客
    思考题的话希望老师能够过后给出一个比较标准的答案,毕竟大家的留言中说法各不相同很难去判断答案的对错

    作者回复: 这一部分的最后一章,要不就给答案吧

    2019-03-09
    11
  • 李可威
    老师为什么按序申请资源就可以破坏循环等待条件呢?这点没有看懂求解答

    作者回复: 循环等待,一定是A->B->C->...->N->A形成环状。
    如果按需申请,是不允许N->A出现的,只能N->P。没有环状,也就不会死锁了。

    2019-03-17
    1
    7
  • gogo
    看了老师的讲解学到了很多,联想了下实际转账业务,应该是数据库来实现的,假如有账户表account,利用mysql的悲观锁select ...for update对a,b两条数据锁定,这时也有可能发生死锁,按照您讲到的第三种破坏循环等待的方式,按照id的大小顺序依次锁定。我这样理解的对吗?

    作者回复: 是的,就是id的次序。

    2019-03-10
    7
  • Howie
    while 循环就是一个自旋锁机制吧,自旋锁的话要关注它的循环时间,不能一直循环下去,不然会浪费 cpu 资源。

    作者回复: 自旋锁在JVM里是一种特殊的锁机制,自诩不会阻塞线程的。咱们这个其实还是会阻塞线程的。不过原理都一样,你这样理解也没问题。

    2019-03-09
    7
  • 陈华
    对于第三点,按资源顺序来锁就能打破循环等待有疑问。
    例如:账户 1 向 账户 3 转账
      同时 账户 3 向 账户 5 转账
    即使按资源顺序来锁,也是起不了啥作用吧!?,

    作者回复: 能起作用,这俩操作不会死锁

    2019-03-14
    1
    6
  • 王二宝
    最常见的就是B转A的同时,A转账给B,那么先锁B再锁A,但是,另一个线程是先锁A再锁B,然而,如果两个线程同时执行,那么就是出现死锁的情况,线程T1锁了A请求锁B,此时线程T2锁了B请求锁A,都在等着对方释放锁,然而自己都不会释放锁,故死锁。
    最简单的办法,就是无论哪个线程执行的时候,都按照顺序加锁,即按照A和B的id大小来加锁,这样,无论哪个线程执行的时候,都会先加锁A,再加锁B,A被加锁,则等待释放。这样就不会被死锁了。

    作者回复: 👍

    2019-08-28
    5
  • aguan(^・ェ・^)
    老师,在破坏占用且等待的案例中,为何申请完两个账户的资源后还需要再分别锁定this和target账户呢?

    作者回复: 为了保险而已,单纯这个例子是不需要的,如果还有取款操作就需要了

    2019-03-14
    5
  • GP
    问下,上节最后说到,不能用可变对象做锁,这里为何又synchronized(left)?

    作者回复: 保护的是对象里面的成员,这俩对象变也只能是里面成员变,相对于里面的成员来说,这俩对象是永远不会变的。你可以这样理解。不是绝对不能用于可变对象,只是一条最佳实践。

    2019-03-13
    5
  • λ
    单例导致操作也是串行的吧

    作者回复: 是串行,但是允许A转B,C转D

    2019-03-11
    1
    4
  • Nero.t.Kang
    虽然看起来 while(!actr.apply(this, target));只是锁住了两个对象,但是因为actr是一个单例的对象,这个方法在执行的时候也需要锁住actr,在多线程状态下也相当于是串行化了,那么这和加上一个Account.class的类锁的串行化有什么区别吗?请老师赐教,谢谢。

    作者回复: 有区别,如果转账操作很耗时,那么a-b,c-d能并行还是有价值的

    2019-03-11
    4
  • winter
    我的想法是,如果Account对象中只有转账业务的话,while(actr.apply(this, target)和对象锁synchronized(Account.class)的性能优势几乎看不出来,synchronized(Account.class)的性能甚至更差;但是如果Account对象中如果还有其它业务,比如查看余额等功能也加了synchronized(Account.class)修饰,那么把单独的转账业务剥离出来,性能的提升可能就比较明显了。

    作者回复: 是的,有时候性能更差,毕竟要synchronized三次。但是有些场景会更好,例如转账操作很慢,而apply很快,这个时候允许a->b,c->d并行就有优势了。

    2019-03-09
    4
  • 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-26
    3
  • 长眉_张永
    关键是如何找到最合适的锁的力度。

    作者回复: 是啊,所以知识只是知识,不是能力

    2019-03-13
    3
收起评论
99+
返回
顶部