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

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

[[175972]]

双重检查锁定的由来

在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的延迟初始化方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面基于类初始化方案的延迟初始化。

责任编辑:赵宁宁 来源: Linux社区
相关推荐

2011-04-20 10:07:15

2013-11-29 09:51:26

C++双重检查锁定

2013-12-23 09:48:43

C++锁定模式

2011-06-17 15:29:44

C#对象初始化器集合初始化器

2022-07-27 08:56:29

Go程序多版本

2023-11-12 23:08:17

C++初始化

2010-02-06 14:40:50

C++初始化和赋值

2012-02-28 10:04:09

Java

2009-11-11 15:29:15

ADO初始化

2019-11-04 13:50:36

Java数组编程语言

2009-09-08 09:48:34

LINQ初始化数组

2010-07-28 10:22:33

FlexApplica

2021-03-12 10:30:11

SpringMVC流程初始化

2022-07-06 10:37:45

SpringServlet初始化

2020-12-03 09:50:52

容器IoC流程

2011-06-09 14:13:06

C++JAVA缺省初始化

2012-04-09 13:43:12

Java

2024-01-15 06:34:09

Gin镜像容器

2009-10-20 14:03:48

VB.NET数组声明VB.NET数组初始化

2009-08-26 18:28:44

C#数组
点赞
收藏

51CTO技术栈公众号