多线程下双重检查锁的问题及解决方法

 单例模式中有一种实现方式叫双重检查锁,主要是为了更好、更安全的实现单例功能。先来看一下该方法的核心代码:  

[java]  view plain  copy
  1. <span style="font-size:18px;">public  class DoubleCheckedLocking{  
  2.   private static Instance instance;                   
  3.    
  4.   public static Instance getInstance(){               
  5.     if(instance ==null){                              
  6.       synchronized (DoubleCheckedLocking.class){      
  7.         if(instance ==null)                           
  8.           instance=new Instance();                  //①  
  9.       }  
  10.     }  
  11.     return instance;  
  12.   }  
  13. }</span>  
    表面上来看,在执行该代码时,先判断instance对象是否为空,为空时再进行初始化对象。即使是在多线程环境下,因为使用了synchronized锁进行代码同步,该方法也仅仅创建一个实例对象。但是,从根本上来说,这样写还是存在一定问题的。

  问题源头:

   上述代码标号为①的代码功能是是创建实例对象,可以分解为如下伪代码步骤: 

[html]  view plain  copy
  1. <span style="font-size:18px;">memory = allocate() ;    //分配对象的内存空间  
  2. ctorInstance(memory);   //②初始化对象  
  3. instance=memory;        //③设置instance指向刚分配的内存地址</span>  

  其中②和③之间,在某些编译器编译时,可能出现重排序(主要是为了代码优化),此时的代码如下:  

[java]  view plain  copy
  1. <span style="font-size:18px;">memory = allocate() ;    //分配对象的内存空间  
  2. instance=memory;        //③设置instance指向刚分配的内存地址  
  3. ctorInstance(memory);   //②初始化对象  
  4. </span>  
   单线程下执行时序图如下:

 

   多线程下执行时序图:

   

   由于单线程中遵守intra-thread semantics,从而能保证即使②和③交换顺序后其最终结果不变。但是当在多线程情况下,线程B将看到一个还没有被初始化的对象,此时将会出现问题。

   解决方案:

    1、不允许②和③进行重排序

    2、允许②和③进行重排序,但排序之后,不允许其他线程看到。


   基于volatile的解决方案

    对前面的双重锁实现的延迟初始化方案进行如下修改:   

[java]  view plain  copy
  1. <span style="font-size:18px;">public  class DoubleCheckedLocking{</span>  
  2. <span style="font-size:18px;">  private volatile static Instance instance;                   
  3.    
  4.   public static Instance getInstance(){               
  5.     if(instance ==null){                              
  6.       synchronized (DoubleCheckedLocking.class){      
  7.         if(instance ==null)                           
  8.           instance=new Instance();                  //用volatile修饰,不会再出现重排序  
  9.       }  
  10.     }  
  11.     return instance;  
  12.   }  
  13. }</span>  
   使用volatile修饰instance之后,之前的②和③之间的重排序将在多线程环境下被禁止,从而保证了线程安全执行。

   注意:这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)


  基于类初始化的解决方案

   JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案。    

[java]  view plain  copy
  1. <span style="font-size:18px;">public class InstanceFactory {  
  2.     private static class InstanceHolder {  
  3.         public static Instance instance = new Instance();  
  4.     }  
  5.   
  6.     public static Instance getInstance() {  
  7.         return InstanceHolder.instance ;  //这里将导致InstanceHolder类被初始化  
  8.     }  
  9. }</span>  
   执行的示意图:    


   该方案的实质是,允许②和③进行重排序,但不允许非构造线程(此处是B线程)“看到”这个重排序。


原链接:点击

 单例模式中有一种实现方式叫双重检查锁,主要是为了更好、更安全的实现单例功能。先来看一下该方法的核心代码:  

[java]  view plain  copy
  1. <span style="font-size:18px;">public  class DoubleCheckedLocking{  
  2.   private static Instance instance;                   
  3.    
  4.   public static Instance getInstance(){               
  5.     if(instance ==null){                              
  6.       synchronized (DoubleCheckedLocking.class){      
  7.         if(instance ==null)                           
  8.           instance=new Instance();                  //①  
  9.       }  
  10.     }  
  11.     return instance;  
  12.   }  
  13. }</span>  
    表面上来看,在执行该代码时,先判断instance对象是否为空,为空时再进行初始化对象。即使是在多线程环境下,因为使用了synchronized锁进行代码同步,该方法也仅仅创建一个实例对象。但是,从根本上来说,这样写还是存在一定问题的。

  问题源头:

   上述代码标号为①的代码功能是是创建实例对象,可以分解为如下伪代码步骤: 

[html]  view plain  copy
  1. <span style="font-size:18px;">memory = allocate() ;    //分配对象的内存空间  
  2. ctorInstance(memory);   //②初始化对象  
  3. instance=memory;        //③设置instance指向刚分配的内存地址</span>  

  其中②和③之间,在某些编译器编译时,可能出现重排序(主要是为了代码优化),此时的代码如下:  

[java]  view plain  copy
  1. <span style="font-size:18px;">memory = allocate() ;    //分配对象的内存空间  
  2. instance=memory;        //③设置instance指向刚分配的内存地址  
  3. ctorInstance(memory);   //②初始化对象  
  4. </span>  
   单线程下执行时序图如下:

 

   多线程下执行时序图:

   

   由于单线程中遵守intra-thread semantics,从而能保证即使②和③交换顺序后其最终结果不变。但是当在多线程情况下,线程B将看到一个还没有被初始化的对象,此时将会出现问题。

   解决方案:

    1、不允许②和③进行重排序

    2、允许②和③进行重排序,但排序之后,不允许其他线程看到。


   基于volatile的解决方案

    对前面的双重锁实现的延迟初始化方案进行如下修改:   

[java]  view plain  copy
  1. <span style="font-size:18px;">public  class DoubleCheckedLocking{</span>  
  2. <span style="font-size:18px;">  private volatile static Instance instance;                   
  3.    
  4.   public static Instance getInstance(){               
  5.     if(instance ==null){                              
  6.       synchronized (DoubleCheckedLocking.class){      
  7.         if(instance ==null)                           
  8.           instance=new Instance();                  //用volatile修饰,不会再出现重排序  
  9.       }  
  10.     }  
  11.     return instance;  
  12.   }  
  13. }</span>  
   使用volatile修饰instance之后,之前的②和③之间的重排序将在多线程环境下被禁止,从而保证了线程安全执行。

   注意:这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)


  基于类初始化的解决方案

   JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案。    

[java]  view plain  copy
  1. <span style="font-size:18px;">public class InstanceFactory {  
  2.     private static class InstanceHolder {  
  3.         public static Instance instance = new Instance();  
  4.     }  
  5.   
  6.     public static Instance getInstance() {  
  7.         return InstanceHolder.instance ;  //这里将导致InstanceHolder类被初始化  
  8.     }  
  9. }</span>  
   执行的示意图:    


   该方案的实质是,允许②和③进行重排序,但不允许非构造线程(此处是B线程)“看到”这个重排序。


原链接:点击

猜你喜欢

转载自blog.csdn.net/u013046597/article/details/77509167
今日推荐