双重检查加锁 与 volatile

一个问题:双重检查加锁为什么用了 volatile 就可以正确工作?

反过来问:不用 volatile 修饰 resource 属性有什么问题?

双重检查加锁的简单示例:

public class DoubleCheckedLocking {
    private static Resource resource;

    public static Resource getInstance() {
        if (resource == null) {     // LL01
            synchronized (DoubleCheckedLocking.class) {
                if (resource == null) {
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

上述代码反编译后的字节码指令如下:

$ javap -c DoubleCheckedLocking.class
Compiled from "DoubleCheckedLocking.java"
public class net.coderbee.dcl.DoubleCheckedLocking {
  public net.coderbee.dcl.DoubleCheckedLocking();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: return

  public static net.coderbee.dcl.Resource getInstance();
    Code:
       0: getstatic     #18                 // Field resource:Lnet/coderbee/dcl/Resource;
       3: ifnonnull     35
       6: ldc           #1                  // class net/coderbee/dcl/DoubleCheckedLocking
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #18                 // Field resource:Lnet/coderbee/dcl/Resource;
      14: ifnonnull     27
      17: new           #20                 // class net/coderbee/dcl/Resource
      20: dup
      21: invokespecial #22                 // Method net/coderbee/dcl/Resource."<init>":()V
      24: putstatic     #18                 // Field resource:Lnet/coderbee/dcl/Resource;
      27: aload_0
      28: monitorexit
      29: goto          35
      32: aload_0
      33: monitorexit
      34: athrow
      35: getstatic     #18                 // Field resource:Lnet/coderbee/dcl/Resource;
      38: areturn
    Exception table:
       from    to  target type
          11    29    32   any
          32    34    32   any
}

最重要的是下面这几行,Java 里创建一个对象包含三个步骤:分配内存、执行初始化、赋值给引用。

17: new           #20                 // class net/coderbee/dcl/Resource
20: dup
21: invokespecial #22                 // Method net/coderbee/dcl/Resource."<init>":()V
24: putstatic     #18                 // Field

由于重排序的原因,可能在初始化之前就把对象赋值给引用,导致访问的线程看到一个未初始化完成的对象。这就是双重检查加锁不用 volatile 的致命问题。

《Java并发编程实践》里提到的一些可能重排序的场景:

  • 编译器生成的指令顺序可以与源码中的顺序不同。
  • 编译器会把变量保存在寄存器中,而不是内存中。
  • 处理器可以采用乱序或并行等方式来执行指令。
  • 缓存可能会改变将写入变量提交到主内存的顺序。
  • 保存在处理器本地缓存中的值,对其他处理器不可见。

结合 DoubleCheckedLocking 代码来看看什么情况下就会出现上述问题:
加入线程A调用 getInstance 方法,发现 resource 引用是空的,就进入同步块,再次判断发现 resource 还是为空,开始创建对象的第一步 分配内存,由于重排序,把新对象的赋值给了 resource,并刷新到了主存;此时另一个线程B 也调用 getInstance 方法,发现 resource 引用非空,直接开始访问 resouce 对象,它访问的对象可能是未初始化完成的。

加上 volatile 可能禁止指令重排序,要求给引用赋值必须在对象初始化完成之后。

synchronized 同步块可能会给人一种假象,以为解决了同步、可见性的问题,它确实可以解决这两个问题,前提是线程都经过同步块。

而双重检查加锁存在不经过同步块的问题。

同步块内的代码仍然是可以重排序的。

对于延迟初始化,推荐的是资源占位符类模式,示例如下:

public class ResourceFactory {
    static class ResourceHolder {
        static Resource resource = new Resource();
    }

    public static Resource getInstance() {
        return ResourceHolder.resource;
    }
}

欢迎关注我的微信公众号: coderbee笔记

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据