单例模式——DCL失效问题
(阅读此文需要花费五分钟)
intra-thread semantics保证重排序不会改变单线程内的程序执行结果。换句话说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序。
ourInstance = new Singleton();
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
ourInstance = memory; // 3:设置ourInstance 指向刚分配的内存地址
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
只要保证2排在4的前面,即使2和3之间重排序了,也不会违反intra-thread semantics。
错误结果
线程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;
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重排序,但不允许其他线程“看到”这个重排序
使用静态内部类的方式
▷也许你还喜欢◁
看完点“在看”,待会儿变好看!ღ