双重检查锁定(以下称为DCL)已被广泛当做多线程环境下延迟初始化的一种高效手段。

遗憾的是,在Java中,如果没有额外的同步,它并不可靠。在其它语言中,如c++,实现DCL,需要依赖于处理器的内存模型、编译器实行的重排序以及编 译器与同步库之间的交互。由于c++没有对这些做出明确规定,很难说DCL是否有效。可以在c++中使用显式的内存屏障来使DCL生效,但Java中并没 有这些屏障。

来看下面的代码

01 // Single threaded version
02 class Foo {
03   private Helper helper = null;
04   public Helper getHelper() {
05     if (helper == null)
06         helper = new Helper();
07     return helper;
08     }
09   // other functions and members...
10 }

如果这段代码用在多线程环境下,有几个可能出错的地方。最明显的是,可能会创建出两或多个Helper对象。(后面会提到其它问题)。将getHelper()方法改为同步即可修复此问题。

01 // Correct multithreaded version
02 class Foo {
03   private Helper helper = null;
04   public synchronized Helper getHelper() {
05     if (helper == null)
06         helper = new Helper();
07     return helper;
08     }
09   // other functions and members...
10 }

上面的代码在每次调用getHelper时都会执行同步操作。DCL模式旨在消除helper对象被创建后还需要的同步。

01 // Broken multithreaded version
02 // "Double-Checked Locking" idiom
03 class Foo {
04   private Helper helper = null;
05   public Helper getHelper() {
06     if (helper == null)
07       synchronized(this) {
08         if (helper == null)
09           helper = new Helper();
10       }   
11     return helper;
12     }
13   // other functions and members...
14 }

不幸的是,这段代码无论是在优化型的编译器下还是在共享内存处理器中都不能有效工作。

不起作用

上面代码不起作用的原因有很多。接下来我们先说几个比较显而易见的原因。理解这些之后,也许你想找出一种方法来“修复”DCL模式。你的修复也不会起作用:这里面有很微妙的原因。在理解了这些原因之后,可能想进一步进行修复,但仍不会正常工作,因为存在更微妙的原因。

很多聪明的人在这上面花费了很多时间。除了在每个线程访问helper对象时执行锁操作别无他法。

不起作用的第一个原因

最显而易见的原因是,Helper对象初始化时的写操作与写入helper字段的操作可以是无序的。这样的话,如果某个线程调用getHelper()可 能看到helper字段指向了一个Helper对象,但看到该对象里的字段值却是默认值,而不是在Helper构造方法里设置的那些值。

如果编译器将调用内联到构造方法中,那么,如果编译器能证明构造方法不会抛出异常或执行同步操作,初始化对象的这些写操作与hepler字段的写操作之间就能自由的重排序。

即便编译器不对这些写操作重排序,在多处理器上,某个处理器或内存系统也可能重排序这些写操作,运行在其它 处理器上的线程就可能看到重排序带来的结果。

Doug Lea写了一篇更详细的有关编译器重排序的文章。

展示其不起作用的测试案例

Paul Jakubik找到了一个使用DCL不能正常工作的例子。下面的代码做了些许整理:

001 public class DoubleCheckTest
002 {
003    
004  
005   // static data to aid in creating N singletons
006   static final Object dummyObject = new Object(); // for reference init
007   static final int A_VALUE = 256; // value to initialize 'a' to
008   static final int B_VALUE = 512; // value to initialize 'b' to
009   static final int C_VALUE = 1024;
010   static ObjectHolder[] singletons;  // array of static references
011   static Thread[] threads; // array of racing threads
012   static int threadCount; // number of threads to create
013   static int singletonCount; // number of singletons to create
014    
015  
016   static volatile int recentSingleton;
017  
018  
019   // I am going to set a couple of threads racing,
020   // trying to create N singletons. Basically the
021   // race is to initialize a single array of
022   // singleton references. The threads will use
023   // double checked locking to control who
024   // initializes what. Any thread that does not
025   // initialize a particular singleton will check
026   // to see if it sees a partially initialized view.
027   // To keep from getting accidental synchronization,
028   // each singleton is stored in an ObjectHolder
029   // and the ObjectHolder is used for
030   // synchronization. In the end the structure
031   // is not exactly a singleton, but should be a
032   // close enough approximation.
033   //
034  
035  
036   // This class contains data and simulates a
037   // singleton. The static reference is stored in
038   // a static array in DoubleCheckFail.
039   static class Singleton
040     {
041     public int a;
042     public int b;
043     public int c;
044     public Object dummy;
045  
046     public Singleton()
047       {
048       a = A_VALUE;
049       b = B_VALUE;
050       c = C_VALUE;
051       dummy = dummyObject;
052       }
053     }
054  
055   static void checkSingleton(Singleton s, int index)
056     {
057     int s_a = s.a;
058     int s_b = s.b;
059     int s_c = s.c;
060     Object s_d = s.dummy;
061     if(s_a != A_VALUE)
062       System.out.println("[" + index + "] Singleton.a not initialized " +
063 s_a);
064     if(s_b != B_VALUE)
065       System.out.println("[" + index
066                          + "] Singleton.b not intialized " + s_b);
067      
068     if(s_c != C_VALUE)
069       System.out.println("[" + index
070                          + "] Singleton.c not intialized " + s_c);
071      
072     if(s_d != dummyObject)
073       if(s_d == null)
074         System.out.println("[" + index
075                            + "] Singleton.dummy not initialized,"
076                            + " value is null");
077       else
078         System.out.println("[" + index
079                            + "] Singleton.dummy not initialized,"
080                            + " value is garbage");
081     }
082  
083   // Holder used for synchronization of
084   // singleton initialization.
085   static class ObjectHolder
086     {
087     public Singleton reference;
088     }
089  
090   static class TestThread implements Runnable
091     {
092     public void run()
093       {
094       for(int i = 0; i < singletonCount; ++i)
095         {
096     ObjectHolder o = singletons[i];
097         if(o.reference == null)
098           {
099           synchronized(o)
100             {
101             if (o.reference == null) {
102               o.reference = new Singleton();
103           recentSingleton = i;
104           }
105             // shouldn't have to check singelton here
106             // mutex should provide consistent view
107             }
108           }
109         else {
110           checkSingleton(o.reference, i);
111       int j = recentSingleton-1;
112       if (j > i) i = j;
113       }
114         }
115       }
116     }
117  
118   public static void main(String[] args)
119     {
120     if( args.length != 2 )
121       {
122       System.err.println("usage: java DoubleCheckFail" +
123                          " <numThreads> <numSingletons>");
124       }
125     // read values from args
126     threadCount = Integer.parseInt(args[0]);
127     singletonCount = Integer.parseInt(args[1]);
128      
129     // create arrays
130     threads = new Thread[threadCount];
131     singletons = new ObjectHolder[singletonCount];
132  
133     // fill singleton array
134     for(int i = 0; i < singletonCount; ++i)
135       singletons[i] = new ObjectHolder();
136  
137     // fill thread array
138     for(int i = 0; i < threadCount; ++i)
139       threads[i] = new Thread( new TestThread() );
140  
141     // start threads
142     for(int i = 0; i < threadCount; ++i)
143       threads[i].start();
144  
145     // wait for threads to finish
146     for(int i = 0; i < threadCount; ++i)
147       {
148       try
149         {
150         System.out.println("waiting to join " + i);
151         threads[i].join();
152         }
153       catch(InterruptedException ex)
154         {
155         System.out.println("interrupted");
156         }
157       }
158     System.out.println("done");
159     }
160 }

当上述代码运行在使用Symantec JIT的系统上时,不能正常工作。尤其是,Symantec JIT将

1 singletons[i].reference = new Singleton();

编译成了下面这个样子(Symantec JIT用了一种基于句柄的对象分配系统)。

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

如你所见,赋值给singletons[i].reference的操作在Singleton构造方法之前做掉了。在现有的Java内存模型下这完全是允许的,在c和c++中也是合法的(因为c/c++都没有内存模型(译者注:这篇文章写作时间较久,c++11已经有内存模型了))。

一种不起作用的“修复”

基于前文解释的原因,一些人提出了下面的代码:

01 // (Still) Broken multithreaded version
02 // "Double-Checked Locking" idiom
03 class Foo {
04   private Helper helper = null;
05   public Helper getHelper() {
06     if (helper == null) {
07       Helper h;
08       synchronized(this) {
09         h = helper;
10         if (h == null)
11             synchronized (this) {
12               h = new Helper();
13             } // release inner synchronization lock
14         helper = h;
15         }
16       }   
17     return helper;
18     }
19   // other functions and members...
20 }

将创建Helper对象的代码放到了一个内部的同步块中。直觉的想法是,在退出同步块的时候应该有一个内存屏障,这会阻止Helper的初始化与helper字段赋值之间的重排序。

很不幸,这种直觉完全错了。同步的规则不是这样的。monitorexit(即,退出同步块)的规则是,在monitorexit前面的action必须 在该monitor释放之前执行。但是,并没有哪里有规定说monitorexit后面的action不可以在monitor释放之前执行。因此,编译器 将赋值操作helper = h;挪到同步块里面是非常合情合理的,这就回到了我们之前说到的问题上。许多处理器提供了这种单向的内存屏障指令。如果改变锁释放的语义 —— 释放时执行一个双向的内存屏障 —— 将会带来性能损失。

更多不起作用的“修复”

可以做些事情迫使写操作的时候执行一个双向的内存屏障。这是非常重量级和低效的,且几乎可以肯定一旦Java内存模型修改就不能正确工作了。不要这么用。如果对此感兴趣,我在另一个网页上描述了这种技术。不要使用它。

但是,即使初始化helper对象的线程用了双向的内存屏障,仍然不起作用。

问题在于,在某些系统上,看到helper字段是非null的线程也需要执行内存屏障。

为何?因为处理器有自己本地的对内存的缓存拷贝。在有些处理器上,除非处理器执行一个cache coherence指令(即,一个内存屏障),否则读操作可能从过期的本地缓存拷贝中取值,即使其它处理器使用了内存屏障将它们的写操作写回了内存。

我开了另一个页面来讨论这在Alpha处理器上是如何发生的。

值得费这么大劲吗?

对于大部分应用来说,将getHelper()变成同步方法的代价并不高。只有当你知道这确实造成了很大的应用开销时才应该考虑这种细节的优化。

通常,更高级别的技巧,如,使用内部的归并排序,而不是交换排序(见SPECJVM DB的基准),带来的影响更大。

让静态单例生效

如果你要创建的是static单例对象(即,只会创建一个Helper对象),这里有个简单优雅的解决方案。

只需将singleton变量作为另一个类的静态字段。Java的语义保证该字段被引用前是不会被初始化的,且任一访问该字段的线程都会看到由初始化该字段所引发的所有写操作。

1 class HelperSingleton {
2     static Helper singleton = new Helper();
3 }

对32位的基本类型变量DCL是有效的

虽然DCL模式不能用于对象引用,但可以用于32位的基本类型变量。注意,DCL也不能用于对long和double类型的基本变量,因为不能保证未同步的64位基本变量的读写是原子操作。

01 // Correct Double-Checked Locking for 32-bit primitives
02 class Foo {
03   private int cachedHashCode = 0;
04   public int hashCode() {
05     int h = cachedHashCode;
06     if (h == 0)
07     synchronized(this) {
08       if (cachedHashCode != 0) return cachedHashCode;
09       h = computeHashCode();
10       cachedHashCode = h;
11       }
12     return h;
13     }
14   // other functions and members...
15 }

事实上,如果computeHashCode方法总是返回相同的结果且没有其它附属作用时(即,computeHashCode是个幂等方法),甚至可以消除这里的所有同步。

01 // Lazy initialization 32-bit primitives
02 // Thread-safe if computeHashCode is idempotent
03 class Foo {
04   private int cachedHashCode = 0;
05   public int hashCode() {
06     int h = cachedHashCode;
07     if (h == 0) {
08       h = computeHashCode();
09       cachedHashCode = h;
10       }
11     return h;
12     }
13   // other functions and members...
14 }

用显式的内存屏障使DCL有效

如果有显式的内存屏障指令可用,则有可能使DCL生效。例如,如果你用的是C++,可以参考来自Doug Schmidt等人所著书中的代码:

01 // C++ implementation with explicit memory barriers
02 // Should work on any platform, including DEC Alphas
03 // From "Patterns for Concurrent and Distributed Objects",
04 // by Doug Schmidt
05 template <class TYPE, class LOCK> TYPE *
06 Singleton<TYPE, LOCK>::instance (void) {
07     // First check
08     TYPE* tmp = instance_;
09     // Insert the CPU-specific memory barrier instruction
10     // to synchronize the cache lines on multi-processor.
11     asm ("memoryBarrier");
12     if (tmp == 0) {
13         // Ensure serialization (guard
14         // constructor acquires lock_).
15         Guard<LOCK> guard (lock_);
16         // Double check.
17         tmp = instance_;
18         if (tmp == 0) {
19                 tmp = new TYPE;
20                 // Insert the CPU-specific memory barrier instruction
21                 // to synchronize the cache lines on multi-processor.
22                 asm ("memoryBarrier");
23                 instance_ = tmp;
24         }
25     return tmp;
26 }

用线程局部存储来修复DCL

Alexander Terekhov (TEREKHOV@de.ibm.com)提出了个能实现DCL的巧妙的做法 —— 使用线程局部存储。每个线程各自保存一个flag来表示该线程是否执行了同步。

01 class Foo {
02  /** If perThreadInstance.get() returns a non-null value, this thread
03     has done synchronization needed to see initialization
04     of helper */
05      private final ThreadLocal perThreadInstance = new ThreadLocal();
06      private Helper helper = null;
07      public Helper getHelper() {
08          if (perThreadInstance.get() == null) createHelper();
09          return helper;
10      }
11      private final void createHelper() {
12          synchronized(this) {
13              if (helper == null)
14                  helper = new Helper();
15          }
16      // Any non-null value would do as the argument here
17          perThreadInstance.set(perThreadInstance);
18      }
19 }

这种方式的性能严重依赖于所使用的JDK实现。在Sun 1.2的实现中,ThreadLocal是非常慢的。在1.3中变得更快了,期望能在1.4上更上一个台阶。Doug Lea分析了一些延迟初始化技术实现的性能

在新的Java内存模型下

JDK5使用了新的Java内存模型和线程规范。

用volatile修复DCL

JDK5以及后续版本扩展了volatile语义,不再允许volatile写操作与其前面的读写操作重排序,也不允许volatile读操作与其后面的读写操作重排序。更多详细信息见Jeremy Manson的博客。

这样,就可以将helper字段声明为volatile来让DCL生效。在JDK1.4或更早的版本里仍是不起作用的。

01 // Works with acquire/release semantics for volatile
02 // Broken under current semantics for volatile
03 class Foo {
04     private volatile Helper helper = null;
05     public Helper getHelper() {
06         if (helper == null) {
07             synchronized(this) {
08                 if (helper == null)
09                     helper = new Helper();
10             }
11         }
12         return helper;
13     }
14 }

不可变对象的DCL

如果Helper是个不可变对象,那么Helper中的所有字段都是final的,那么不使用volatile也能使DCL生效。主要是因为指向不可变对象的引用应该表现出形如int和float一样的行为;读写不可变对象的引用是原子操作。

文章转自 并发编程网-ifeve.com

有关“双重检查锁定失效”的说明相关推荐

  1. java 双重检查锁 失效_关于多线程:为什么Java中双重检查锁定被打破?

    此问题与旧Java版本的行为以及双重检查锁定算法的旧实现有关 较新的实现使用volatile并依赖于稍微改变的volatile语义,因此它们没有损坏. 声明字段分配始终是原子的,除了long或doub ...

  2. Java盲点:双重检查锁定及单例模式

    时间:2009-08-11 19:20    来源:未知    作者:Peter Haggar 核心提示:Peter Haggar , 高级软件工程师, IBM 2004 年 5 月 01 日 所有的 ...

  3. 双重检查锁定及单例模式

                                         双重检查锁定及单例模式 转载 http://www.ibm.com/developerworks/cn/java/j-dcl. ...

  4. C++和双重检查锁定模式(DCLP)的风险

    转自: http://blog.jobbole.com/86392/ 多线程其实就是指两个任务一前一后或者同时发生. 1 简介 当你在网上搜索设计模式的相关资料时,你一定会找到最常被提及的一个模式:单 ...

  5. java 双重检查锁定及单例模式

    双重检查锁定及单例模式 全面理解这一失效的编程习语 Peter Haggar , 高级软件工程师, IBM Peter Haggar 是 IBM 在北卡罗来纳州的 Research Triangle ...

  6. C++ 从双重检查锁定问题 到 内存屏障的一些思考

    文章目录 1. 问题描述 2. DCLP 的问题 和 指令执行顺序 2.1 Volatile 关键字 2.2 C++11 的内存模型 3. C++11内存模型 解决DCLP问题 3.1 内存屏障和获得 ...

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

    双重检查锁定的由来 在Java程序中,有时需要推迟一些高开销的对象的初始化操作,并且只有在真正使用到这个对象的时候,才进行初始化,此时,就需要延迟初始化技术. 延迟初始化的正确实现是需要一些技巧的,否 ...

  8. 单例-双重检查锁定与延迟初始化

    在java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实现线程安全的延迟初始化需要一些技巧,否则很容易出现问题. ...

  9. singleton 类_在Java中对Singleton类进行双重检查锁定

    singleton 类 Singleton类在Java开发人员中非常常见,但是它给初级开发人员带来了许多挑战. 他们面临的主要挑战之一是如何使Singleton保持为Singleton? 也就是说,无 ...

最新文章

  1. ADT启动SDK Manager时一闪而过
  2. Web安全——正方教务系统自主选课非正常退课解决方案(危险操作,仅用于学习)
  3. 从手机App通过WebSocket向浏览器推送数据
  4. 我们是如何做DevOps的?
  5. Python函数参数传递:传值还是传引用
  6. 8号团队-团队任务三:每日立会(2018-11-27)
  7. Phaser3让超级玛丽实现轻跳、高跳及加上对应的跳跃声音
  8. 中兴通讯:将在全球范围内发布近10款5G手机
  9. BZOJ 1070: [SCOI2007]修车(费用流)
  10. java如何使用while_java中的while(true)语句的用法是什么
  11. php将abc转换成整形是什么意思,php强制转换类型的方法
  12. python2和python3的编码区别
  13. EasyUI的DataGrid 打印导出
  14. 一篇很好的面试文章 程序员面试IT公司的33个小贴士
  15. RobotFramework中实现接口上传文件
  16. bootstrapform表单重置_“bootstrap table”怎么重置表单?
  17. rMATs 分析可变剪切
  18. 在Excel中如何让文字竖排
  19. 【转】Windows批处理学习(一)——MS-DOS命令
  20. 利用轻量级js插件Beer Slider实现新老图片的实时对比

热门文章

  1. windows安装TortoiseGit详细使用教程【基础篇】
  2. SQLite数据库存储
  3. 图像检索系统 Image Retrieval Systems
  4. APP启动速度是门面,如何做到极致优化?
  5. SSM 框架 Maven项目整合实例
  6. 12.6 Nginx安装 12.7 默认虚拟主机 12.8 Nginx用户认证 12.9 Nginx
  7. 单例模式-3.透明的单例模式
  8. C++ 强制类型转换(转载)
  9. Android仿微信图片上传,可以选择多张图片,缩放预览,拍照上传等
  10. iOS 带IAP提交注意事项及无法submit for review的解决方案