多线程版本的if不同于单线程中的if,是需要等待的,执着的等到条件为真。快速放弃是与之相对的场景。
各种编辑器提供的自动保存功能是快速放弃的一个常见例子。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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; } }
|
AutoSaveEditor这个类不是线程安全的,因为对共享变量changed的读写没有使用同步。
读写共享变量changed的方法autoSave()和edit()都加互斥锁即可,但是性能很差,锁的范围太大。
将锁的范围缩小,只在读写共享变量changed的地方加锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void autoSave() { synchronized(this) { if(!changed) { return; } changed = false; } this.execSave(); }
void edit() [ ... synchronized(this) [ changed = true; ] ]
|
示例中的共享变量是一个状态变量,业务逻辑依赖于这个状态变量的状态:当状态满足某个条件时,执行某个业务逻辑,其本质是一个if,放到多线程场景里,就是一种“多线程版本的if”。这种多线程版本的if的场景对应Balking模式。
Balking模式的经典实现
Balking模式本质上是一种规范化的解决“多线程版本的if”的方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| boolean changed = false;
void autoSave() { synchronized(this) { if(!changed) { return; } changed = false; } this.execSave(); }
void edit() { ... change(); }
void change() { synchronized(this) { changed = true; } }
|
自动保存的例子使用Balking模式规范化之后,仅仅是将edit()方法中队共享变量changed的赋值操作抽取到了change()中,将并发处理逻辑和业务逻辑分开。
用volatile实现Balking模式
用synchronized实现的Balking模式,最为稳妥。在某些特定场景下,可以使用volatile来实现,但使用volatile的前提是对原子性没有要求。
Copy-on-Write中的Balking模式示例
Copy-on-Write中,RPC框架路由表的案例,在RPC框架中,本地路由表是要和注册中心进行信息同步的,应用启动的时候,会将应用依赖服务的路由表从注册中心同步到本地路由表中,如果应用重启的时候注册中心宕机,那么会导致该应用依赖的服务军不可用,因为找不到依赖服务的路由表。为了防止这种极端情况出现,RPC框架可以将本地路由表自动保存到本地文件中,如果重启的时候注册中心宕机,那么就从本地文件中恢复重启前的路由表。这是一种降级的方案。
自动保存路由表和编辑器自动保存原理是一样的,也可以用Balking模式实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class RouterTable { 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的写操作不存在原子性的要求(boolean变量读写是一条机器指令完成的;ConcurrentHashMap和CopyOnWriteArraySet的写操作也是线程安全的),而且采用scheduleWithFixedDelay()这种调度方式能保证同一时刻只有一个线程执行autoSave()方法(即使多个线程执行,也不影响最终结果)。
Balking模式典型应用场景-单次初始化。
1 2 3 4 5 6 7 8 9 10 11
| class InitTest { boolean inited = false; synchronized void init() { if(inited) { return; } doInit(); inited = true; } }
|
将init()声明为一个同步方法,这样同一个时刻就只有一个线程能够执行inti()方法;init()方法在第一次执行完时会将inited设置为true,这样后续执行init()方法的线程就不会再执行doInit()了。
线程安全的单例模式本质上其实也是单次初始化,可以用Balking模式来实现线程安全的单例模式。
1 2 3 4 5 6 7 8 9 10 11 12
| class Singleton { private static Singleton singleton; private Singleton() {} public synchronized static Singleton getInstance() { if(singleton == null) { singleton = new Singleton(); } return singleton; } }
|
互斥锁synchronized将getInstance()方法串行化了,性能很差。
采取双重检查(Double Check)方案优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Singleton { private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if(singleton==null) { synchronized(Singleton.class) { if(singleton==null) { singleton = new Singleton(); } } } return singleton(); } }
|
在双重检查方案中,一旦Singleton对象被成功创建之后,就不会执行synchronized(Singleton.class){}相关的代码,第一次检查避免执行加锁操作,此时getInstance()方法的执行路径是无锁的,从而解决了性能问题。获取锁后的二次检查对安全性负责。使用volatile禁止编译优化。
ReadWriteLock中实现缓存按需加载功能使用了双重检查方案。
总结
Balking模式和Guarded Suspension模式从实现上看没有多大的关系,Balking模式只需要用互斥锁就能解决,而Guarded Suspension模式则要用到管程这种高级的并发原语;但是从应用的角度来看,它们解决的都是“线程安全的if”语义,不同之处在于,Guarded Suspension模式会等待if条件为真,而Balking模式不会等待。
Balking模式的经典实现是使用互斥锁,可以使用Java语言内置synchronized,也可以使用SDK提供Lock;也可以尝试采用volatile方案()。