推荐阅读
并发设计模式
并发设计模式 | Guarded Suspension模式:等待唤醒机制的规范实现
并发设计 | Immutability模式:解决并发安全问题
并发工具
25 | CompletionService:批量执行异步任务
24 | CompletableFuture:Java异步编程
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?
上一篇文章 并发设计模式 | Guarded Suspension模式:等待唤醒机制的规范实现 中,我们提到可以用“多线程版本的 if”来理解 Guarded Suspension 模式,不同于单线程中的 if,这个“多线程版本的 if”是需要等待的,而且还很执着,必须要等到条件为真。但很显然这个世界,不是所有场景都需要这么执着,有时候我们还需要快速放弃 —— 条件不满足,就立即终止。
“条件不满足,就立即终止” 加个if判断不就行了,条件/状态不合适提前返回就行了。没错,如此通用的方法,被前人总结为一个有趣的名字 —— Balking模式。
工作开发中,在非预期的条件、或当前不适合执行这个操作/没必要执行这个操作,就停止处理,直接返回的例子很多。
编辑器自动保存功能
class AutoSaveEditor{
// 文件是否被修改过
boolean changed=false;
// 定时任务线程池
ScheduledExecutorService ses =
Executors.newSingleThreadScheduledExecutor();
// 定时执行自动保存
void startAutoSave(){
ses.scheduleWithFixedDelay(()->{
autoSave();
}, 7, 7, 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;
}
}
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;
}
}
前面我们用 synchronized 实现了 Balking 模式,这种实现方式最为稳妥,建议你实际工作中也使用这个方案。不过在某些特定场景下,也可以使用 volatile 来实现,但使用 volatile 的前提是对原子性没有要求。
在 Copy-on-Write模式:不是延时策略的COW 中,有一个 RPC 框架路由表的案例,在 RPC 框架中,本地路由表是要和注册中心进行信息同步的,应用启动的时候,会将应用依赖服务的路由表从注册中心同步到本地路由表中,如果应用重启的时候注册中心宕机,那么会导致该应用依赖的服务均不可用,因为找不到依赖服务的路由表。为了防止这种极端情况出现,RPC 框架可以将本地路由表自动保存到本地文件中,如果重启的时候注册中心宕机,那么就从本地文件中恢复重启前的路由表。这其实也是一种降级的方案。
自动保存路由表和前面介绍的编辑器自动保存原理是一样的,也可以用 Balking 模式实现,不过我们这里采用 volatile 来实现,实现的代码如下所示。
//路由表信息
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;
}
}
之所以可以采用 volatile 来实现,是因为对共享变量 changed 和 rt 的写操作不存在原子性的要求,而且采用 scheduleWithFixedDelay() 这种调度方式能保证同一时刻只有一个线程执行 autoSave() 方法。
Balking 模式有一个非常典型的应用场景就是单次初始化,下面的示例代码是它的实现。这个实现方案中,我们将 init() 声明为一个同步方法,这样同一个时刻就只有一个线程能够执行 init() 方法;init() 方法在第一次执行完时会将 inited 设置为 true,这样后续执行 init() 方法的线程就不会再执行 doInit() 了。
class SingleInit {
boolean inited = false;
synchronized void init(){
if(inited){
return;
}
// 省略 doInit 的实现
doInit();
inited=true;
}
}
线程安全的单例模式本质上其实也是单次初始化,所以可以用 Balking 模式来实现线程安全的单例模式,下面的示例代码是其实现。这个实现虽然功能上没有问题,但是性能却很差,因为互斥锁 synchronized 将 getInstance() 方法串行化了。
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;
}
}
当前最优解是使用经典的双重检查
,不过需要你注意的是,这个方案中使用了 volatile 来禁止编译优化,其原因你可以参考 可见性、原子性和有序性问题:并发编程Bug的源头 编译优化带来的有序性问题。
何时使用Balking模式 ?
推荐阅读
JUC源码
本文首发于 公众号 架构道与术(ToBeArchitecturer),欢迎关注、学习更多干货~
推荐阅读
并发设计模式
并发设计模式 | Guarded Suspension模式:等待唤醒机制的规范实现
并发设计 | Immutability模式:解决并发安全问题
并发工具
25 | CompletionService:批量执行异步任务
24 | CompletableFuture:Java异步编程
19 | CountDownLatch和CyclicBarrier让多线程步调一致
17 | ReadWriteLock:如何快速实现一个完备的缓存?