双重检验锁与单例模式与volatitle

在网上看到过好多篇文章在说明双重检查锁在多个线程初始化一个单例类时到底为什么不行时在关键位置的描述模棱两可,今天我们就来看一下为什么不能用双重检查锁,问题到底出在了那里?


下面我们直接进入主题,为什么使用双重检查锁,原因是因为在多线程初始化一个单例类时我们要确保得到一个对象,又想再确保一个对象时得到更高的效率,所以就有了双重检查锁,使用双重检查锁初始化对象的代码如下

[java]  view plain  copy
  1. public class DoubleCheckedLocking {                 //1  
  2.     private static DoubleCheckedLocking instance;                   //2  
  3.   
  4.     public static DoubleCheckedLocking getInstance() {              //3  
  5.         if (instance == null) {                                     //4:第一次检查  
  6.             synchronized (DoubleCheckedLocking.class) {             //5:加锁  
  7.                 if (instance == null)                               //6:第二次检查  
  8.                     instance = new DoubleCheckedLocking();          //7:问题的根源出在这里  
  9.             }                                                       //8  
  10.         }                                                           //9  
  11.         return instance;                                            //10  
  12.     }                                                               //11  
  13. }      
为什么这样是不行的,问题的根源出在第7行(instance = new DoubleCheckedLocking();),创建一个对象可以分解为如下三步:

[java]  view plain  copy
  1. memory = allocate();   //1:分配对象的内存空间  
  2. ctorInstance(memory);  //2:初始化对象  
  3. instance = memory;     //3:设置instance指向刚分配的内存地址  
上面三行伪代码中的2和3之间,可能会被重排序,2和3之间重排序之后的执行时序如下:


[java]  view plain  copy
  1. memory = allocate();   //1:分配对象的内存空间  
  2. instance = memory;     //3:设置instance指向刚分配的内存地址  
  3.                        //注意,此时对象还没有被初始化!  
  4. ctorInstance(memory);  //2:初始化对象  
重排序不能影响单线程的执行语义,虽然这里2和3进行了重排序,但是只要保证2排在4前面执行,单线程内的执行结果不会被改变

时间线程A线程B
t1A1:分配对象的内存空间 
t2A3:设置instance指向内存空间 
t3 B1:判断instance是否为空
t4 B2:由于instance不为null,线程B将访问instance引用的对象(而这个时候对象还没有初始化)
t5A2:初始化对象 
t6A4:访问instance引用的对象 
线程B拿到一个未初始化的对象去操作,结果肯定就出错了


总结,到此为止我们只说明了为什么不可以用双重检查锁来初始化对象

THE  END!!!




 

java中双检锁为什么要加上volatile关键字


单线程版本:

[java]  view plain  copy
  1. class Foo {   
  2.   private Helper helper = null;  
  3.   public Helper getHelper() {  
  4.     if (helper == null)   
  5.         helper = new Helper();  
  6.     return helper;  
  7.     }  
  8.   // other functions and members...  
  9.   }  

多线程版本(正确的):

class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }
多线程版本(错误的):

[java]  view plain  copy
  1. class Foo {   
  2.   private Helper helper = null;  
  3.   public Helper getHelper() {  
  4.     if (helper == null)   
  5.       synchronized(this) {  
  6.         if (helper == null)   
  7.           helper = new Helper();  
  8.       }      
  9.     return helper;  
  10.     }  
  11.   // other functions and members...  
  12.   }  

使用volatile关键字修改版:

[java]  view plain  copy
  1. class Foo {  
  2.     private volatile Helper helper = null;  
  3.     public Helper getHelper() {  
  4.         if (helper == null) {  
  5.             synchronized (this) {  
  6.                 if (helper == null)  
  7.                     helper = new Helper();  
  8.             }  
  9.         }  
  10.         return helper;  
  11.     }  
  12. }  


为什么不加volatile的双检锁是不起作用的?

The most obvious reason it doesn't work it that the writes that initialize the Helper object and the write to the helper field can be done or perceived out of order. Thus, a thread which invokes getHelper() could see a non-null reference to a helper object, but see the default values for fields of the helper object, rather than the values set in the constructor.


If the compiler inlines the call to the constructor, then the writes that initialize the object and the write to the helper field can be freely reordered if the compiler can prove that the constructor cannot throw an exception or perform synchronization.

Even if the compiler does not reorder those writes, on a multiprocessor the processor or the memory system may reorder those writes, as perceived by a thread running on another processor.


在给helper对象初始化的过程中,jvm做了下面3件事:

1.给helper对象分配内存

2.调用构造函数

3.将helper对象指向分配的内存空间

由于jvm的"优化",指令2和指令3的执行顺序是不一定的,当执行完指定3后,此时的helper对象就已经不在是null的了,但此时指令2不一定已经被执行。

假设线程1和线程2同时调用getHelper()方法,此时线程1执行完指令1和指令3,线程2抢到了执行权,此时helper对象是非空的。

所以线程2拿到了一个尚未初始化的helper对象,此时线程2调用这个helper就会抛出异常。


为什么volatile可以一定程度上保证双检锁ok?


1.volatile关键字可以保证jvm执行的一定的“有序性”,在指令1和指令2执行完之前,指定3一定不会被执行。
   为什么说是一定的"有序性"呢,因为对于非易失的读写,jvm仍然允许对volatile变量进行乱序读写

2.保证了volatile变量被修改后立刻刷新会驻内存中。

参考资料:
1.http://www.javaworld.com/article/2074979/java-concurrency/double-checked-locking--clever--but-broken.html
2.http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
3.http://www.importnew.com/18126.html



  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值