Java并发编程实战
王宝令
资深架构师
立即订阅
15161 人已学习
课程目录
已完结 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并发编程实战
登录|注册

04 | 互斥锁(下):如何用一把锁保护多个资源?

王宝令 2019-03-07
在上一篇文章中,我们提到受保护资源和锁之间合理的关联关系应该是 N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源,并且结合文中示例,我们也重点强调了“不能用多把锁来保护一个资源”这个问题。而至于如何保护多个资源,我们今天就来聊聊。
当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。

保护没有关联关系的多个资源

在现实世界里,球场的座位和电影院的座位就是没有关联关系的,这种场景非常容易解决,那就是球赛有球赛的门票,电影院有电影院的门票,各自管理各自的。
同样这对应到编程领域,也很容易解决。例如,银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的。
相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(111)

  • 树森
    有个疑问,使用Account.class获得锁,那所有转账操作不是都成串行了,这里实践中可行吗?

    作者回复: 不可行,下一期讲优化

    2019-03-07
    1
    83
  • senekis
    解决原子性问题,是要保证中间状态对外不见

    太精辟了!
    2019-03-07
    3
    75
  • 少主江衫
    用this.balance 和this.password 都不行。在同一个账户多线程访问时候,A线程取款进行this.balance-=amt;时候此时this.balance对应的值已经发生变换,线程B再次取款时拿到的balance对应的值并不是A线程中的,也就是说不能把可变的对象当成一把锁。this.password 虽然说是String修饰但也会改变,所以也不行。老师所讲的例子中的两个Object无论多次访问过程中都未发生变化?
    请老师指正。

    作者回复: 正确,不能用可变对象做锁

    2019-03-07
    65
  • 夜空中最亮的星(华仔)
    我是一名普通的运维工程师,我是真看不懂java代码,我是来听思想的 。

    作者回复: 那我就放心了

    2019-03-07
    2
    38
  • 老杨同志
    思考题:
    我觉得不能用balance和password做为锁对象。这两个对象balance是Integer,password是String都是不可变变对象,一但对他们进行赋值就会变成新的对象,加的锁就失效了

    作者回复: 是的

    2019-03-07
    3
    25
  • 别皱眉
    老师,很感谢有这个专栏,让我能够更加系统的学习并发知识。
    对于思考题,之所以不可行是因为每次修改balance和password时都会使锁发生变化。
    -----------------------------------------------------------------------
    以下只是我的猜想
    比如有线程A、B、C
    线程A首先拿到balance1锁,线程B这个时候也过来,发现锁被拿走了,线程B被放入一个地方进行等待。
    当A修改掉变量balance的值后,锁由balance1变为balance2.
    线程B也拿到那个balance1锁,这时候刚好有线程C过来,拿到了balance2锁。
    由于B和C持有的锁不同,所以可以同时执行这个方法来修改balance的值,这个时候就有可能是线程B修改的值会覆盖掉线程C修改的值?
    -----------------------------------------------------------------------
    不知道到底是不是这样?老师可以详细讲下这个过程吗?谢谢

    作者回复: 你分析的很仔细了,就是这样的,bc锁的不是一个对象。不能保证互斥性

    2019-03-13
    1
    16
  • yuc
    是否可以在Account中添加一个静态object,通过锁这个object来实现一个锁保护多个资源,如下:
    class Account {
      private static Object lock = new Object();
      private int balance;
      // 转账
      void transfer(Account target, int amt){
        synchronized(lock) {
          if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
          }
        }
      }
    }

    作者回复: 这种方式比锁class更安全,因为这个缺是私有的。有些最佳实践要求必须这样做。👍

    2019-03-09
    3
    12
  • 强哥
    文章里第二个例子根本无法用到实践中,锁力度太大,可以用乐观关锁解决,另外分布式的情况下,应该如何分析也应该讲讲?至于原子性其实跟数据库的原子性还是有差异的,例如虚拟机异常退出时,synchinzed也无法操作原子操作的。

    作者回复: 分布式的不讲了,分支太多不好。下一期会讲优化

    2019-03-07
    1
    11
  • zhaozp
    可变对象不能作为锁

    作者回复: 总结的到位

    2019-03-07
    8
  • 星辰
    王老师, 您在第二讲中贴出的英文链接的地址很棒,看着您写过的专栏,再去看它,有种恍然大悟地感觉~! 恳请您还是在后续地专栏里,继续保持这种死磕并发基础地原汁原味地链接啊~! 您地专栏是您多年地理解与实战的营养,加上您亲自地朗读,当然也是原汁原味。但是我的意思是,我们应该有一批人很少看英文类的文档,所以才会有这种恳请~! 谢谢老师~!

    作者回复: 感谢盛赞,我会继续保持的

    2019-03-07
    7
  • wang
    不可以。因为balance为integer对象,当值被修改相当于换锁,还有integer有缓存-128到127,相当于同一个对象。

    作者回复: 深刻!👍

    2019-03-07
    6
  • 思考题,我的答案是不行,因为对象可变,所以导致加锁对象不一样。

    然后感觉加锁的所有用户用同一个锁的粒度太大了,但如果每次转账操作,是不是可以同时加两个用户的锁,如果有先后顺序又可能有死锁问题。

    作者回复: 下一期会讲这个

    2019-03-07
    6
  • 忠艾一生
    这两把锁是会变的,所以无法保证互斥性。在第一个线程执行完之后,this.balance与this.password这两个对象锁都与第一个线程的对象锁是不一样的。 所以是不正确的。

    作者回复: 回答正确

    2019-03-07
    4
  • zyl
    请问这个画图软件是什么?谢谢

    作者回复: 我已经确认过了,是PPT

    2019-03-07
    4
  • 0bug
    思考题:
    结论:不可行
    原因:举个例子,假如this.balance = 10 ,多个线程同时竞争同一把锁this.balance,此时只有一个线程拿到了锁,其他线程等待,拿到锁的线程进行this.balance -= 1操作,this.balance = 9。 该线程释放锁, 之前等待锁的线程继续竞争this.balance=10的锁,新加入的线程竞争this.balance=9的锁,导致多个锁对应一个资源

    作者回复: 分析的很仔细👍

    2019-03-07
    4
  • 水如天
    以前碰到一个坑,线程上下文的类加载器改变了,导致前后加载的类不一致

    作者回复: osgi?

    2019-03-07
    3
  • walkingonair
    使用Account.class获得锁有很明显的性能问题,而如何解决这个性能问题恰恰是我想知道的。我的一个想法是利用String对象的intern方法生成转账相关的字符串,利用这个字符串作为锁,这个方案不知道在实践中是否可行?
    另外,狂战俄洛伊同学提出的锁两个对象的示例,我看着像是经典的死锁案例,因加锁顺序不一致导致的死锁,当A->B,A等待B的锁,而当B->A,B等待A的锁,产生死锁,不知理解的是否有问题,欢迎老师和各位同学指教。

    作者回复: 下一期介绍优化

    2019-03-07
    3
  • linqw
    老师,有个疑问,锁是为了保证在多个线程操作,每个线程的操作之前多能先读取到其余线程的操作结果,保证数据的一致性,防止数据被覆盖,但是为什么锁不能使用可变的对象作为锁,Integer和String都是不可变对象,操作都会生成新对象,但是这个会影响到多个线程对数据的操作结果么?比如取款,虽然取款完锁对象改变,但是在并发的时候,不管是其他线程在前一个线程取款前获取到的锁还是取款后获取到的锁,对数据都不会覆盖呀?老师帮忙解答下哦

    作者回复: 多把锁保护一个资源,起不到互斥作用,就可能同时取款,这样数据就被覆盖了。

    2019-03-09
    2
  • 不靠谱的琴谱
    之前评论可以忽略了一个因素,每次赋值都改变了这个对象。但是我有一个疑问 integer在-128~127之间 这个锁是不是有效的

    作者回复: 不是有效的,其他功能也可能用到这个锁

    2019-03-07
    2
  • 狂战俄洛伊
    首先,对于文中的课后思考,这个问题肯定是不行的。
    synchronized加锁的对象是根据地址来的,只要地址变了,锁也就变了。
    当balance或者password发生变化的时候(地址变化),其他线程还是能获得锁,然后执行转账的操作。
    所以这里加锁的对象一定要是地址不能变的资源,例如文中的Account.class这个对象地址就不会变化,所以可以用来作为锁。
    但是问题来了,如果用Account.class作为加锁对象的话,那所有的转账功能将会是串行。实际上4个不同账户之间的转账是可以并行执行的,例如A给B转账,C给D转账,这两个动作是可以一起执行的。
    为了解决这个问题,我对转账代码做了些调整,目的是为了让不同账户之间的转账可以并发运行。下面是我对转账改进后的代码:
    // 转账
    void transfer( Account target, int amt){
    synchronized(this) {
    synchronized (target) {
    if (this.balance > amt) {
    this.balance -= amt;
    target.balance += amt;
    }
    }
    }
    }

    首先这段代码是线程安全的。

    但是我实际测试下来性能却反而不如直接对Account.class加锁高,请问老师这是为什么呢?

    作者回复: 转账里面sleep一段时间就有效果了

    2019-03-07
    2
    2
收起评论
99+
返回
顶部