Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

为什么Java中不能使用Double Checked Locking模式

Posted on 2004-07-08 11:06  Flier Lu  阅读(5196)  评论(0编辑  收藏  举报
http://www.blogcn.com/user8/flier_lu/index.html?id=1620823&run=.0C124A1

  OFBiz项目的主要人员之一David Jones在一篇讨论中,Re: double-checked synch, Entity Engine & Struts,讨论了为什么不在OFBiz中使用Struts和Double Checked Locking模式。
     Double Checked Locking 模式是一种非常有效的对 Singleton 模式的优化方法。
     
     一般来说,Singleton模式可以通过如下代码简单实现:
 
以下为引用:

 // Single threaded version
 class Foo 
 { 
   private Helper helper = null;
   public Helper getHelper() {
     if (helper == null) 
         helper = new Helper();
     return helper;
     }
   // other functions and members...
 }
 

    
     而为了支持多线程时可能存在的并发访问,需要进行锁定保护
 
以下为引用:

 // Correct multithreaded version
 class Foo 
 { 
   private Helper helper = null;
   public synchronized Helper getHelper() {
     if (helper == null) 
         helper = new Helper();
     return helper;
     }
   // other functions and members...
 }
 

    
     这样的锁定实际上只在第一次访问getHelper函数并初始化Helper对象这个时间段有效,其他时间都是冗余的。因此在C++/C#等语言中大量使用了 Double-Checked Locking 模式进行优化,如
 
以下为引用:

 // Broken multithreaded version
 // "Double-Checked Locking" idiom
 class Foo 
 { 
   private Helper helper = null;
   public Helper getHelper() {
     if (helper == null) 
       synchronized(this) {
         if (helper == null) 
           helper = new Helper();
       }    
     return helper;
     }
   // other functions and members...
 }
 

    
     具体的模式原理和分析,可以参考关于ACE中此模式使用的文章。
     
     ACE中的Double Checked Locking 模式
     
     但在 Java 中,这种使用方法实际上是错误的
     
     The "Double-Checked Locking is Broken" Declaration
     
     这是因为 Java 语言规范和 C++/C# 等不同,是一个非常灵活的规范。Java 编译器可以自由地重排变量的初始化和访问顺序,以提高运行时效率;同时 Java 中的变量访问是可以被自动缓存到寄存器的,这也导致潜在的 JIT 编译器相关的依赖性错误。每个编译器可能有不同的实现方法,同样的代码在不同编译器和执行环境下可能有不同的表现,呵呵
     关于 Java 基于编译器的变量重排和内存模型的相关知识可以参考
     
     Synchronization and the Java Memory Model
     
     就目前来说,一种可行的解决办法是使用线程局部存储来保存 Singleton 实例,避免 JIT 对其进行优化
 
以下为引用:

 class Foo 
 {
  /** If perThreadInstance.get() returns a non-null value, this thread
  has done synchronization needed to see initialization of helper */

   private final ThreadLocal perThreadInstance = new ThreadLocal();  
   private Helper helper = null;
   
   public Helper getHelper() 
   {
      if (perThreadInstance.get() == null) createHelper();
      
      return helper;
   }
   
   private final void createHelper() 
   {
      synchronized(this) 
      {
          if (helper == null)
              helper = new Helper();
      }
   // Any non-null value would do as the argument here
      perThreadInstance.set(perThreadInstance);
   }
 }
 


    
     而要彻底解决这个问题,还是得等 JSR 133 提供的volatile 关键字,用于修饰变量的可变性,强制屏蔽编译器和 JIT 的优化工作。
 
以下为引用:

 // Works with acquire/release semantics for volatile
 // Broken under current semantics for volatile
 class Foo 
 {
   private volatile Helper helper = null;
   public Helper getHelper() 
   {
     if (helper == null) 
     {
       synchronized(this) 
       {
         if (helper == null)
           helper = new Helper();
       }
     }
     return helper;
   }
 }
 

   
     更详尽的资料可以参考 The Java Memory Model