Java中的volatile在使用双层检查实现单例模式的解读
1.前言
因为今天在想到这个问题的时候脑子不是很清楚,就想查一下网上的资料,结果发现一个个写的囫囵吞枣。后来突然想起来了,于是打算记录下来。
注意此种方法只针对JDK1.5及以上,之前好像是volatile的关键字设计有问题?
2.双层检查实现单例模式的由来
最开始只有一层检查,
public
Resource getResource() {
if
(resource ==
null
) {
resource =
new
Resource();
}
return
resource;
}
public
synchronized Resource getResource() {
if
(resource ==
null
) {
resource =
new
Resource();
}
return
resource;
}
public
Resource getResource() {
if
(resource ==
null
) {
synchronized
(
this
){
if
(resource==
null
) {
resource =
new
Resource();
}
}
}
return
resource;
}
如果加了volatile关键字,这个方法是有效的。如果没有,这个方法是有一定多线程风险的。
现在进入本文的重点,此处为什么不加volatile有风险。
首先声明:
此处利用了volatile的一个关键字特性:防止局部指令重排序
简单的概括就是,
volatile禁止指令重排序的一些规则:2
1.当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
2.当第一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
下面来看new Resource()这个操作,
它可以分解成三个操作,
【伪代码】3
1.memory = allocate()
2.createInstance(memory)
3.resource = memory
其中1,2指令有数据依赖,所以不会被重排序。而2,3指令没有数据依赖,如果没有volatile关键字,可能会被重排序。
那么假如2,3执行顺序进行了调换。
那么就有可能发生,假设A,B线程都即将执行getResource操作,目前在A线程,
首先A线程第一次判断resource是否为null,结果为null,那么加锁进入执行创建对象的这三步。
假设执行了上述的1,3后,发生了调度,B开始执行。
此时它面临的状态是,判断resource是否为null,结果不为null.因为刚才A执行了3已经给resource赋值了。
那么B会认为resource已经初始化完成,它可能要对这个对象进行一些操作,但是事实上A还没有执行2操作,resource对象还没有初始化完成。
这样运行下去可能会产生异常,风险由此产生。
不过加了volatile后,在执行对resource写之前,volatile关键字保证了1,2,3操作不会重排序(需要考证),这样就保证了resource要么为null,要么就是一个完整的resource对象。
顺便补充一下,加了volatile之后,即使在执行1,2,3的时候发生了调度,因为锁在A线程手里,所以B线程没有办法拿到锁进行初始化。所以即使调度时候resource为null,也不会发生多次初始化的情形。
如果需要对volatile有更多的了解,
可以访问官方Java Language Specification
关于volatile: http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#d5e12277
关于Java内存模型: http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4
参考文档:
1.http://www.jb51.net/article/80201.htm
2.https://blog.csdn.net/hqq2023623/article/details/51013468
3.http://blog.csdn.net/xiakepan/article/details/52444565