【翻译十一】“double-checked locking”的问题

原创
2017/03/26 21:41
阅读数 526

Does the new memory model fix the "double-checked locking" problem?

关于double-checked locking的问题,必读这篇文章《双重检查锁定与延迟初始化》

The (infamous) double-checked locking idiom (also called the multithreaded singleton pattern) is a trick designed to support lazy initialization while avoiding the overhead of synchronization. In very early JVMs, synchronization was slow, and developers were eager to remove it -- perhaps too eager. The double-checked locking idiom looks like this:

Double-checked locking,也叫多线程单例模式,是用来在延迟初始化对象的时候,避免同步的开销,因为在早期的JVM中,同步是很慢的。

    // double-checked-locking - don't do this!
    
    private static Something instance = null;
    
    public Something getInstance() {
      if (instance == null) {
        synchronized (this) {
          if (instance == null)
            //此处,初始化和赋值引用可能重排序
            instance = new Something();
        }
      }
      return instance;
    }

This looks awfully clever -- the synchronization is avoided on the common code path. There's only one problem with it -- it doesn't work. Why not? The most obvious reason is that the writes which initialize instance and the write to the instance field can be reordered by the compiler or the cache, which would have the effect of returning what appears to be a partially constructed Something. The result would be that we read an uninitialized object. There are lots of other reasons why this is wrong, and why algorithmic corrections to it are wrong. There is no way to fix it using the old Java memory model. More in-depth information can be found at Double-checked locking: Clever, but broken and The "Double Checked Locking is broken" declaration

以上是一个double-checked locking,尽管看起来很完美,使用普通代码就避免掉了同步,但其实并不起作用。

原因是,instance = new Something();相当于以下三步

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置instance指向内存空间

其中,2和3可能重排序。详见文献 Double-checked locking: Clever, but brokenThe "Double Checked Locking is broken" declaration

Many people assumed that the use of the volatile keyword would eliminate the problems that arise when trying to use the double-checked-locking pattern. In JVMs prior to 1.5, volatile would not ensure that it worked (your mileage may vary). Under the new memory model, making the instance field volatile will "fix" the problems with double-checked locking, because then there will be a happens-before relationship between the initialization of the Something by the constructing thread and the return of its value by the thread that reads it.

一些人用volatile来解决使用double-checked-locking单例模式带来的问题,但在1.5之前,volatile也未必有用。但在新内存模型中,给instance加个volatile修饰符则可以解决上述问题,因为JMM设置了一个happen-before关系:构造线程对instance的初始化 happen before 其它线程读取这个对象。

 事实上,volatile是通过禁止上述2和3重排序实现的。

Instead, use the Initialization On Demand Holder idiom, which is thread-safe and a lot easier to understand:

相反的,使用Demand Holder来线程安全的完成初始化,会容易好多,Demand Holder如下

private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}

This code is guaranteed to be correct because of the initialization guarantees for static fields; if a field is set in a static initializer, it is guaranteed to be made visible, correctly, to any thread that accesses that class.

用以上方法来实现单例是正确的,因为类在初始化是静态字段时,对任何线程都是可见的。

What if I'm writing a VM?

You should look at http://gee.cs.oswego.edu/dl/jmm/cookbook.html .

Why should I care?

Why should you care? Concurrency bugs are very difficult to debug. They often don't appear in testing, waiting instead until your program is run under heavy load, and are hard to reproduce and trap. You are much better off spending the extra effort ahead of time to ensure that your program is properly synchronized; while this is not easy, it's a lot easier than trying to debug a badly synchronized application.

我为什么要care?并发bug是很难改的,一般测试不出来,只有在程序负载很大的时候才会出现,而且很难复现。花大精力做好同步,总比改bug好啊~

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部