单例模式——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。这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。

只要保证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;

Volatile的重排序规则:

  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
如何实现volatile的内存语义:JMM采取保守策略
下面是基于保守策略的JMM内存屏障插入策略:
  1. 在每个volatile写操作的前面插入一个StoreStore屏障。
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障。
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障。
  4. 在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
 

b. 基于类初始化的解决方案--即允许2和3重排序,但不允许其他线程“看到”这个重排序

使用静态内部类的方式
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章