0%

Balking模式:线程安全的单例模式

多线程版本的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的前提是对原子性没有要求。

Balking模式典型应用场景-单次初始化。

1
2
3
4
5
6
7
8
9
10
11
class InitTest {
boolean inited = false;
synchronized void init() {
if(inited) {
return;
}
//省略doInit的实现
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方案()。