上一篇文章中,我们提到可以用“多线程版本的 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 模式。

Balking 模式的经典实现

Balking 模式本质上是一种规范化地解决“多线程版本的 if”的方案,对于上面自动保存的例子,使用 Balking 模式规范化之后的写法如下所示,你会发现仅仅是将 edit() 方法中对共享变量 changed 的赋值操作抽取到了 change() 中,这样的好处是将并发处理逻辑和业务逻辑分开。

boolean changed=false;
//自动存盘操作
void autoSave(){synchronized(this){if (!changed) {return;}changed = false;}//执行存盘操作//省略且实现this.execSave();
}
//编辑操作
void edit(){//省略编辑逻辑......change();
}
//改变状态
void change(){synchronized(this){changed = true;}
}

用 volatile 实现 Balking 模式

前面我们用 synchronized 实现了 Balking 模式,这种实现方式最为稳妥,建议你实际工作中也使用这个方案。不过在某些特定场景下,也可以使用 volatile 来实现,但使用 volatile 的前提是对原子性没有要求。
在《Copy-on-Write 模式》中,有一个 RPC 框架路由表的案例,在 RPC 框架中,本地路由表是要和注册中心进行信息同步的,应用启动的时候,会将应用依赖服务的路由表从注册中心同步到本地路由表中,如果应用重启的时候注册中心宕机,那么会导致该应用依赖的服务均不可用,因为找不到依赖服务的路由表。为了防止这种极端情况出现,RPC 框架可以将本地路由表自动保存到本地文件中,如果重启的时候注册中心宕机,那么就从本地文件中恢复重启前的路由表。这其实也是一种降级的方案。
自动保存路由表和前面介绍的编辑器自动保存原理是一样的,也可以用 Balking 模式实现,不过我们这里采用 volatile 来实现,实现的代码如下所示。之所以可以采用 volatile 来实现,是因为对共享变量 changed 和 rt 的写操作不存在原子性的要求,而且采用 scheduleWithFixedDelay() 这种调度方式能保证同一时刻只有一个线程执行 autoSave() 方法。

//路由表信息
public class RouterTable {//Key:接口名//Value:路由集合ConcurrentHashMap<String, CopyOnWriteArraySet<Router>> rt = new ConcurrentHashMap<>();    //路由表是否发生变化volatile boolean changed;//将路由表写入本地文件的线程池ScheduledExecutorService ses=Executors.newSingleThreadScheduledExecutor();//启动定时任务//将变更后的路由表写入本地文件public void startLocalSaver(){ses.scheduleWithFixedDelay(()->{autoSave();}, 1, 1, MINUTES);}//保存路由表到本地文件void autoSave() {if (!changed) {return;}changed = false;//将路由表写入本地文件//省略其方法实现this.save2Local();}//删除路由public void remove(Router router) {Set<Router> set=rt.get(router.iface);if (set != null) {set.remove(router);//路由表已发生变化changed = true;}}//增加路由public void add(Router router) {Set<Router> set = rt.computeIfAbsent(route.iface, r -> new CopyOnWriteArraySet<>());set.add(router);//路由表已发生变化changed = true;}
}

Balking 模式有一个非常典型的应用场景就是单次初始化,下面的示例代码是它的实现。这个实现方案中,我们将 init() 声明为一个同步方法,这样同一个时刻就只有一个线程能够执行 init() 方法;init() 方法在第一次执行完时会将 inited 设置为 true,这样后续执行 init() 方法的线程就不会再执行 doInit() 了。

class InitTest{boolean inited = false;synchronized void init(){if(inited){return;}//省略doInit的实现doInit();inited=true;}
}

线程安全的单例模式本质上其实也是单次初始化,所以可以用 Balking 模式来实现线程安全的单例模式,下面的示例代码是其实现。这个实现虽然功能上没有问题,但是性能却很差,因为互斥锁 synchronized 将 getInstance() 方法串行化了,那有没有办法可以优化一下它的性能呢?

class Singleton{private static Singleton singleton;//构造方法私有化  private Singleton() {}//获取实例(单例)public synchronized static Singleton getInstance(){if(singleton == null){singleton=new Singleton();}return singleton;}
}

办法当然是有的,那就是经典的双重检查(Double Check)方案,下面的示例代码是其详细实现。在双重检查方案中,一旦 Singleton 对象被成功创建之后,就不会执行 synchronized(Singleton.class){}相关的代码,也就是说,此时 getInstance() 方法的执行路径是无锁的,从而解决了性能问题。不过需要你注意的是,这个方案中使用了 volatile 来禁止编译优化,其原因你可以参考《01 | 可见性、原子性和有序性问题:并发编程 Bug 的源头》中相关的内容。至于获取锁后的二次检查,则是出于对安全性负责。

class Singleton{private static volatile Singleton singleton;//构造方法私有化  private Singleton() {}//获取实例(单例)public static Singleton getInstance() {//第一次检查if(singleton==null){synchronize{Singleton.class){//获取锁后二次检查if(singleton==null){singleton=new Singleton();}}}return singleton;}
}

总结

Balking 模式和 Guarded Suspension 模式从实现上看似乎没有多大的关系,Balking 模式只需要用互斥锁就能解决,而 Guarded Suspension 模式则要用到管程这种高级的并发原语;但是从应用的角度来看,它们解决的都是“线程安全的 if”语义,不同之处在于,Guarded Suspension 模式会等待 if 条件为真,而 Balking 模式不会等待。
Balking 模式的经典实现是使用互斥锁,你可以使用 Java 语言内置 synchronized,也可以使用 SDK 提供 Lock;如果你对互斥锁的性能不满意,可以尝试采用 volatile 方案,不过使用 volatile 方案需要你更加谨慎。
当然你也可以尝试使用双重检查方案来优化性能,双重检查中的第一次检查,完全是出于对性能的考量:避免执行加锁操作,因为加锁操作很耗时。而加锁之后的二次检查,则是出于对安全性负责。双重检查方案在优化加锁性能方面经常用到,例如《17 | ReadWriteLock:如何快速实现一个完备的缓存?》中实现缓存按需加载功能时,也用到了双重检查方案。

Java并发编程实战~Balking模式相关推荐

  1. Java并发编程实战~Thread-Per-Message模式

    我们曾经把并发编程领域的问题总结为三个核心问题:分工.同步和互斥.其中,同步和互斥相关问题更多地源自微观,而分工问题则是源自宏观.我们解决问题,往往都是从宏观入手,在编程领域,软件的设计过程也是先从概 ...

  2. Java并发编程实战~Copy-on-Write模式

    Copy-on-Write 模式的应用领域 1.在操作系统领域.类 Unix 的操作系统中创建进程的 API 是 fork(),传统的 fork() 函数会创建父进程的一个完整副本 2.很多文件系统也 ...

  3. Java并发编程实战~Immutability模式

    解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作.这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式.所谓不变性,简单来讲,就是对 ...

  4. 《Java 并发编程实战》--读书笔记

    Java 并发编程实战 注: 极客时间<Java 并发编程实战>–读书笔记 GitHub:https://github.com/ByrsH/Reading-notes/blob/maste ...

  5. Java并发编程实战笔记2:对象的组合

    设计线程安全的类 在设计现车让安全类的过程之中,需要包含以下三步: 找出构成对象状态的所有变量 找出约束状态变量的不变性条件 建立对象状态的并发访问策略 实例封闭 通过封闭机制与合适的加锁策略结合起来 ...

  6. Java并发编程实战_不愧是领军人物!这种等级的“Java并发编程宝典”谁能撰写?...

    前言 大家都知道并发编程技术就是在同一个处理器上同时的去处理多个任务,充分的利用到处理器的每个核心,最大化的发挥处理器的峰值性能,这样就可以避免我们因为性能而产生的一些问题. 大厂的核心负载肯定是非常 ...

  7. java并发编程实战学习(3)--基础构建模块

    转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...

  8. 视频教程-Java并发编程实战-Java

    Java并发编程实战 2018年以超过十倍的年业绩增长速度,从中高端IT技术在线教育行业中脱颖而出,成为在线教育领域一匹令人瞩目的黑马.咕泡学院以教学培养.职业规划为核心,旨在帮助学员提升技术技能,加 ...

  9. 【极客时间】《Java并发编程实战》学习笔记

    目录: 开篇词 | 你为什么需要学习并发编程? 内容来源:开篇词 | 你为什么需要学习并发编程?-极客时间 例如,Java 里 synchronized.wait()/notify() 相关的知识很琐 ...

最新文章

  1. IIS 部署 node.js ---- 基础安装部署
  2. 15.看板方法——启动看板变革笔记
  3. Dataset:机器学习和深度学习中对数据集进行高级绘图(数据集可视化,箱线图等)的简介、应用之详细攻略——daidingdaiding
  4. (Mybatis)日志工厂
  5. BugkuCTF-reverse:入门逆向
  6. ROS学习笔记-ROS语音识别与语音输出[1]
  7. git checkout 单个文件_git 如何回退单个文件
  8. linux下进程监控6,Linux进程监控技术—精通软件性能测试与LoadRunner最佳实战(6)...
  9. Java 并发编程AQS--源码解读
  10. 真正的创业者和伪创业者的区别在哪里?
  11. BNUOJ 34978 汉诺塔 (概率dp)
  12. VMware虚拟桌面,后台更改用户密码后,掉域的问题
  13. 罗技G29方向盘Mac驱动
  14. 惠普服务器开机无限重启,惠普笔记本无限重启的有效解决办法
  15. iOS8官方推荐图标和图像尺寸
  16. 爱的5种能力,你有吗?
  17. PostMan 快快走开, ApiFox 来了, ApiFox 强大的Api调用工具
  18. 录音音频如何转换为mp3格式
  19. java word 批注_Java 添加Word批注(文本、图片)
  20. word编号格式“图 一-1”改为“图 1-1”

热门文章

  1. Android官方开发文档Training系列课程中文版:管理系统UI之变暗系统条
  2. Android官方开发文档Training系列课程中文版:使用Fragment构建动态UI之构建灵活的UI
  3. Exploiting the Syntax-Model Consistency for Neural Relation Extraction(关系抽取,语法模型,跨领域关系抽取
  4. H5常用拖放事件解析
  5. #35 string(缩点+动态规划)
  6. spring基础整理
  7. input框自动填充内容背景颜色为黄色解决方法
  8. mysql truncate table命令使用总结
  9. 计算机网络(一)-概述
  10. 7-5 列车厢调度 (25 分)