双重检查锁定和延迟初始化

双重检查锁定和延迟初始化


双重检查锁定的由来

在Java程序中,有时需要推迟一些高开销的对象的初始化操作,并且只有在真正使用到这个对象的时候,才进行初始化,此时,就需要延迟初始化技术。

延迟初始化的正确实现是需要一些技巧的,否则容易出现问题,下面一一介绍。

方案1


  1. public class UnsafeLazyInit{ 
  2. private static Instance instance; 
  3.  
  4.   public static Instance getInstance(){ 
  5.     if (instance == null){ 
  6.          instance = new Instance(); 
  7.      } 
  8.      return instance; 
  9.  } 
  10. }   

这种做法的错误是很明显的,如果两个线程分别调用getInstance,由于对共享变量的访问没有做同步,很容易出现下面两种情况:

1.线程A和B都看到instance没有初始化,于是分别进行了初始化。

2.instance=new Instance操作被重排序,实际执行过程可能是:先分配内存,然后赋值给instance,最后再执行初始化。如果是这样的话,其他线程可能就会读取到尚未初始化完成的instance对象。

方案2


  1. public class UnsafeLazyInit{ 
  2. private static Instance instance; 
  3.  
  4. public static synchronized Instance getInstance(){ 
  5.     if (instance == null){ 
  6.          instance = new Instance(); 
  7.      } 
  8.      return instance; 
  9.  } 

这种做法的问题是很明显的,每一次读取instance都需要同步,可能会对性能产生较大的影响。

方案3

方案3是一个错误的双重检测加锁实现,看代码:


  1. public class UnsafeLazyInit{ 
  2. private static Instance instance; 
  3.  
  4. public static Instance getInstance(){ 
  5.     if (instance == null){ 
  6.          synchronized(UnsafeLazyInit.classs){ 
  7.              if (instance == null){ 
  8.                   instance = new Instance(); 
  9.                } 
  10.           } 
  11.      } 
  12.      return instance; 
  13.   } 

这种方案看似解决了上面两种方案都存在的问题,但是也是有问题的。

问题根源

instance = new Instance();

这一条语句在实际执行中,可能会被拆分程三条语句,如下:


  1. memory = allocate(); 
  2. ctorInstance(memory); //2 
  3. instance = memory; //3 

根据重排序规则,后两条语句不存在数据依赖,因此是可以进行重排序的。

重排序之后,就意味着,instance域在被赋值了之后,指向的对象可能尚未初始化完成,而instance域是一个静态域,可以被其他线程读取到,那么其他线程就可以读取到尚未初始化完成的instance域。

基于volatile的解决方案

要解决这个办法,只需要禁止语句2和语句3进行重排序即可,因此可以使用volatile来修改instance就能做到了。

private volatile static Instance instance;

因为Volatile语义会禁止编译器将volatile写之前的操作重排序到volatile之后。

基于类初始化的解决方案

Java语言规范规定,对于每一个类或者接口C ,都有一个唯一的初始化锁LC与之对应,从C到LC的映射,由JVM实现。每个线程在读取一个类的信息时,如果此类尚未初始化,则尝试获取LC去初始化,如果获取失败则等待其他线程释放LC。如果能获取到LC,则要判断类的初始化状态,如果是位初始化,则要进行初始化。如果是正在初始化,则要等待其他线程初始化完成,如果是已经初始化,则直接使用此类对象。


  1. public class InstanceFactory{ 
  2.     private static class InstanceHolder{ 
  3.         public static Instance = new Instance(); 
  4.      } 
  5.       
  6.     public static Instance getInstance(){ 
  7.         return InstanceHolder.instance; //这里将导致instance类被初始化 
  8.     }     

结论

字段延迟初始化降低了初始化类或者创建实例的开销,但是增加零访问被延迟促使化的字段的开销。在大部分时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面基于类初始化方案的延迟初始化。


作者:humc

来源:51CTO