vlambda博客
学习文章列表

单例模式——DCL失效问题

(阅读此文需要花费五分钟)

根据Java语言规范,所有线程在执行Java程序时必须要遵守intra-thread semantics。
intra-thread semantics保证重排序不会改变单线程内的程序执行结果。换句话说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序。
ourInstance = new Singleton();
创建了一个对象。这一行代码可以分解为如下的3行伪代码:
memory = allocate();  // 1:分配对象的内存空间ctorInstance(memory); // 2:初始化对象ourInstance = memory; // 3:设置ourInstance 指向刚分配的内存地址
2和3之间重排序之后的执行时序如下:
memory = allocate();  // 1:分配对象的内存空间instance = memory;   // 3:设置instance指向刚分配的内存地址  // 注意,此时对象还没有被初始化!ctorInstance(memory); // 2:初始化对象
2和3之间虽然被重排序了,但这个重排序并不会违反intra-thread semantics。这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。
单例模式——DCL失效问题

只要保证2排在4的前面,即使2和3之间重排序了,也不会违反intra-thread semantics。
单例模式——DCL失效问题
单例模式——DCL失效问题
错误结果
线程A的intra-thread semantics没有改变,但A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。
实现线程安全的延迟初始化的解决办法
a. 不允许2和3重排序。
b. 允许2和3重排序,但不允许其他线程“看到”这个重排序。
a. 基于volatile的解决方案--即不允许2和3重排序
只需做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。
当声明对象的引用为volatile后,2和3之间的重排序,在多线程环境中将会被禁止。

private volatile static Instance instance;
Volatile的重排序规则:
a、当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之
b、当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
c、当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

如何实现volatile的内存语义:JMM采取保守策略
下面是基于保守策略的JMM内存屏障插入策略:
    在每个volatile写操作的前面插入一个StoreStore屏障。
    在每个volatile写操作的后面插入一个StoreLoad屏障。
    在每个volatile读操作的后面插入一个LoadLoad屏障。
    在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
 
b. 基于类初始化的解决方案--即允许2和3重排序,但不允许其他线程“看到”这个重排序
使用静态内部类的方式

也许你还喜欢



看完点“在看”,待会儿变好看!