Java 并发编程实战
王宝令
资深架构师
71384 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 51 讲
学习攻略 (1讲)
Java 并发编程实战
15
15
1.0x
00:00/00:00
登录|注册

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

上一篇文章中,我们提到可以用“多线程版本的 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/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(38)

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

    作者回复: 👍

    3
    57
  • leon
    思考题代码相当于: if(intied == false) { // 1 inited = true; //2 count = calc() } 可能有多条线程同时到1的位置,判断到inited为false,都进入2执行。 解决方案: (1)加锁保护临界区 (2) AtomicBoolean.compareAndSet(false, true)

    作者回复: 👍

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

    作者回复: 👍

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

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

    4
    17
  • 岥羽
    老师,自动保存路由表用 Balking 模式的volatile方式实现中,为什么对共享变量 changed 和 rt 的写操作不存在原子性的要求?

    作者回复: boolean变量读写是一条机器指令完成的

    2
    5
  • Jxin
    volative修饰的属性。我见过在方法中。用局部变量接收该属性值,方法后续的操作都基于该局部变量。这样是不是就不再有volative的特性了?性能虽然提高了,毕竟能走缓存和编译优化了。但是就像上例双重检查的场景。这么个操作就依旧会有空指针异常的可能。请问老师我理解对吗。

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

    5
  • 回答问题: 有问题,volatile不能保证原子性,题目要求只需计算一次Count,所以需要对共享变量inited加锁保护。 疑问: public class RouterTable 类中AutoSave方法同一时刻只有一个线程调用,而Remove和Add方法也是要求使用方单线程访问吗?在实际开发中一般采用什么方式达成这种约定呢?

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

    5
  • 热台
    回答问题 1,cal()可能被执行多次 2. 也可能cal()执行结束前,count就被使用 解决方法 inited 赋值和cal()执行放在一个同步块中,并增加双重check

    作者回复: 👍

    3
  • J.M.Liu
    有问题,存在竞态条件

    作者回复: 👍

    3
  • geoxs
    我有个问题,如果需求不要求只执行一次呢,比如计算很简单,耗费资源不大,多计算几次是可以接受的,可不可以这样写,有没有并发问题呢,甚至我把volitale关键字去掉可不可以呢?

    作者回复: 有没有并发问题可以多看看第一部分

收起评论
显示
设置
留言
38
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部