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

32 | Balking模式:再谈线程安全的单例模式

王宝令 2019-05-11
上一篇文章中,我们提到可以用“多线程版本的 if”来理解 Guarded Suspension 模式,不同于单线程中的 if,这个“多线程版本的 if”是需要等待的,而且还很执着,必须要等到条件为真。但很显然这个世界,不是所有场景都需要这么执着,有时候我们还需要快速放弃。
需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。下面的示例代码将自动保存功能代码化了,很显然 AutoSaveEditor 这个类不是线程安全的,因为对共享变量 changed 的读写没有使用同步,那如何保证 AutoSaveEditor 的线程安全性呢?
class AutoSaveEditor{
//文件是否被修改过
boolean changed=false;
//定时任务线程池
ScheduledExecutorService ses =
Executors.newSingleThreadScheduledExecutor();
//定时执行自动保存
void startAutoSave(){
ses.scheduleWithFixedDelay(()->{
autoSave();
}, 5, 5, TimeUnit.SECONDS);
}
//自动存盘操作
void autoSave(){
if (!changed) {
return;
}
changed = false;
//执行存盘操作
//省略且实现
this.execSave();
}
//编辑操作
void edit(){
//省略编辑逻辑
......
changed = true;
}
}
解决这个问题相信你一定手到擒来了:读写共享变量 changed 的方法 autoSave() 和 edit() 都加互斥锁就可以了。这样做虽然简单,但是性能很差,原因是锁的范围太大了。那我们可以将锁的范围缩小,只在读写共享变量 changed 的地方加锁,实现代码如下所示。
//自动存盘操作
void autoSave(){
synchronized(this){
if (!changed) {
return;
}
changed = false;
}
//执行存盘操作
//省略且实现
this.execSave();
}
//编辑操作
void edit(){
//省略编辑逻辑
......
synchronized(this){
changed = true;
}
}
如果你深入地分析一下这个示例程序,你会发现,示例中的共享变量是一个状态变量,业务逻辑依赖于这个状态变量的状态:当状态满足某个条件时,执行某个业务逻辑,其本质其实不过就是一个 if 而已,放到多线程场景里,就是一种“多线程版本的 if”。这种“多线程版本的 if”的应用场景还是很多的,所以也有人把它总结成了一种设计模式,叫做 Balking 模式
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(25)

  • zero
    是有问题的,volatile关键字只能保证可见性,无法保证原子性和互斥性。所以calc方法有可能被重复执行。

    作者回复: 👍

    2019-05-11
    18
  • leon
    思考题代码相当于:
    if(intied == false) { // 1
         inited = true; //2
         count = calc()
    }

    可能有多条线程同时到1的位置,判断到inited为false,都进入2执行。
    解决方案:
    (1)加锁保护临界区
    (2) AtomicBoolean.compareAndSet(false, true)

    作者回复: 👍

    2019-05-14
    13
  • Corner
    最好就不要单独使用volatile防止产生线程安全问题。因为变量的读写是两个操作,和我们的直觉不一样,很容易出问题。老师的那个volatile就没有问题吗?如果一个线程修改了路由表,此时定时器任务判断共享变量为true,在将其修改为false之前,此时另一个线程又修改了路由表,然后定时任务继续执行会将其修改为false,这就出现问题了。最后还是要在autoSave方法上做同步的。

    作者回复: 定时器任务只有一个线程,autosave加不加同步就无所谓了,多保存一次也没关系,这种概率毕竟很小

    2019-05-11
    4
  • 郑晨Cc
    第8行 inited = true;改成cas操作
    失败直接return。成功继续执行cal方法
    2019-05-11
    3
  • 岥羽
    老师,自动保存路由表用 Balking 模式的volatile方式实现中,为什么对共享变量 changed 和 rt 的写操作不存在原子性的要求?
    2019-08-07
    2
  • Jxin
    volative修饰的属性。我见过在方法中。用局部变量接收该属性值,方法后续的操作都基于该局部变量。这样是不是就不再有volative的特性了?性能虽然提高了,毕竟能走缓存和编译优化了。但是就像上例双重检查的场景。这么个操作就依旧会有空指针异常的可能。请问老师我理解对吗。

    作者回复: 局部变量是不会在线程间共享的,也没有volatile特性

    2019-06-16
    2
  • 孙志强
    inited变量需要使用CAS的方式进行赋值,赋值失败就return,保证只有一个线程可以修改inited变量。

    作者回复: 👍

    2019-05-14
    2
  • 热台
    回答问题
    1,cal()可能被执行多次
    2. 也可能cal()执行结束前,count就被使用

    解决方法
    inited 赋值和cal()执行放在一个同步块中,并增加双重check

    作者回复: 👍

    2019-05-11
    1
  • 刘晓林
    有问题,存在竞态条件

    作者回复: 👍

    2019-05-11
    1
  • 回答问题:
    有问题,volatile不能保证原子性,题目要求只需计算一次Count,所以需要对共享变量inited加锁保护。

    疑问:
    public class RouterTable 类中AutoSave方法同一时刻只有一个线程调用,而Remove和Add方法也是要求使用方单线程访问吗?在实际开发中一般采用什么方式达成这种约定呢?

    作者回复: 你没有办法控制调用方的线程数,autosave你是能控制的。不过加锁以后就串行了

    2019-05-11
    1
  • 李湘河
    Volatile只能保证可见性,无法保证互斥性
    2019-11-24
  • DFighting
    volatile没办法保证操作的原子性,也就是在第一个线程执行inited=true时可能会发生时间片的切换,导致下一个线程在判断是否初始化的时候继续执行calc()操作,也就是说没办法保证calc()的唯一性,对比老师给的代码,应该为整个方法加上synchronized以达到只计算一次calc()的目的
    2019-09-29
  • 逆流的鱼
    这两个模式怎么这么违和,突兀,虎头虎脑的
    2019-08-30
  • 熊熊周周
    inited = true;
    除非代码所工作的操作系统平台环境或者java官方指定这个操作是原子性操作,线程安全的。我们不应该把它当做原子性的操作,线程安全性的操作。
    解决了原始数据类型赋值是否是原子性操作的疑问
    2019-06-21
    1
  • 奇奇
    课后思考题应该是!inited 代码是错的
    2019-06-05
  • points
    class Test{

    AtomicBoolean inited = new AtomicBoolean(false);

    void inited( ){
    if( inited.getAndSet(true) ){
    return ;
    }

    }
    }
    2019-05-31
  • Rancood
    这个Balking模式的好处就是将并发处理逻辑与业务逻辑分离吗
    2019-05-22
  • 贺宇
    这个问题好像和信号量那章的问题很相似
    2019-05-21
  • ZOU志伟
    竞态条件问题
    2019-05-18
  • 星辰
    没有锁 有共享变量 多个线程 可能同时读到false哇, 就可能有多个线程init而让count值超过1哇。

    尽管读到了init=false, 真正的cal()也应该在同步里面,并且init此时任然是false哇~
    2019-05-16
收起评论
25
返回
顶部