没有 volatile 的双重检查锁定

Double-checked locking without volatile

我阅读了 this question 关于如何进行双重检查锁定的内容:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
    FieldType result = field;
    if (result == null) { // First check (no locking)
        synchronized(this) {
            result = field;
            if (result == null) // Second check (with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

我的目标是在没有 volatile 属性的情况下延迟加载字段(不是单例)。字段对象在初始化后永远不会改变。

经过一些测试我的最终方法:

    private FieldType field;

    FieldType getField() {
        if (field == null) {
            synchronized(this) {
                if (field == null)
                    field = Publisher.publish(computeFieldValue());
            }
        }
        return fieldHolder.field;
    }



public class Publisher {

    public static <T> T publish(T val){
        return new Publish<T>(val).get();
    }

    private static class Publish<T>{
        private final T val;

        public Publish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }
}

好处是由于不需要 volatile,访问时间可能会更快,同时仍然保持可重用 Publisher 的简单性 class。


我使用 jcstress 对此进行了测试。 SafeDCLFinal 按预期工作,而 UnsafeDCLFinal 不一致(如预期)。在这一点上,我 99% 确定它有效,但请证明我错了。使用 mvn clean install -pl tests-custom -am 和 运行 使用 java -XX:-UseCompressedOops -jar tests-custom/target/jcstress.jar -t DCLFinal 编译。下面的测试代码(主要是修改过的单例测试 classes):

/*
 * SafeDCLFinal.java:
 */

package org.openjdk.jcstress.tests.singletons;

public class SafeDCLFinal {

    @JCStressTest
    @JCStressMeta(GradingSafe.class)
    public static class Unsafe {
        @Actor
        public final void actor1(SafeDCLFinalFactory s) {
            s.getInstance(SingletonUnsafe::new);
        }

        @Actor
        public final void actor2(SafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new));
        }
    }

    @JCStressTest
    @JCStressMeta(GradingSafe.class)
    public static class Safe {
        @Actor
        public final void actor1(SafeDCLFinalFactory s) {
            s.getInstance(SingletonSafe::new);
        }

        @Actor
        public final void actor2(SafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonSafe::new));
        }
    }


    @State
    public static class SafeDCLFinalFactory {
        private Singleton instance; // specifically non-volatile

        public Singleton getInstance(Supplier<Singleton> s) {
            if (instance == null) {
                synchronized (this) {
                    if (instance == null) {
//                      instance = s.get();
                        instance = Publisher.publish(s.get(), true);
                    }
                }
            }
            return instance;
        }
    }
}

/*
 * UnsafeDCLFinal.java:
 */

package org.openjdk.jcstress.tests.singletons;

public class UnsafeDCLFinal {

    @JCStressTest
    @JCStressMeta(GradingUnsafe.class)
    public static class Unsafe {
        @Actor
        public final void actor1(UnsafeDCLFinalFactory s) {
            s.getInstance(SingletonUnsafe::new);
        }

        @Actor
        public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new));
        }
    }

    @JCStressTest
    @JCStressMeta(GradingUnsafe.class)
    public static class Safe {
        @Actor
        public final void actor1(UnsafeDCLFinalFactory s) {
            s.getInstance(SingletonSafe::new);
        }

        @Actor
        public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonSafe::new));
        }
    }

    @State
    public static class UnsafeDCLFinalFactory {
        private Singleton instance; // specifically non-volatile

        public Singleton getInstance(Supplier<Singleton> s) {
            if (instance == null) {
                synchronized (this) {
                    if (instance == null) {
//                      instance = s.get();
                        instance = Publisher.publish(s.get(), false);
                    }
                }
            }
            return instance;
        }
    }
}

/*
 * Publisher.java:
 */

package org.openjdk.jcstress.tests.singletons;

public class Publisher {

    public static <T> T publish(T val, boolean safe){
        if(safe){
            return new SafePublish<T>(val).get();
        }
        return new UnsafePublish<T>(val).get();
    }

    private static class UnsafePublish<T>{
        T val;

        public UnsafePublish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }

    private static class SafePublish<T>{
        final T val;

        public SafePublish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }
}

已使用 java 8 进行测试,但至少应适用于 java 6+。 See docs


但我想知道这是否可行:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldHolder fieldHolder = null;
    private static class FieldHolder{
        public final FieldType field;
        FieldHolder(){
            field = computeFieldValue();
        }
    }

    FieldType getField() {
        if (fieldHolder == null) { // First check (no locking)
            synchronized(this) {
                if (fieldHolder == null) // Second check (with locking)
                    fieldHolder = new FieldHolder();
            }
        }
        return fieldHolder.field;
    }

或者甚至:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldType field = null;
    private static class FieldHolder{
        public final FieldType field;

        FieldHolder(){
            field = computeFieldValue();
        }
    }

    FieldType getField() {
        if (field == null) { // First check (no locking)
            synchronized(this) {
                if (field == null) // Second check (with locking)
                    field = new FieldHolder().field;
            }
        }
        return field;
    }

或:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldType field = null;

    FieldType getField() {
        if (field == null) { // First check (no locking)
            synchronized(this) {
                if (field == null) // Second check (with locking)
                    field = new Object(){
                        public final FieldType field = computeFieldValue();
                    }.field;
            }
        }
        return field;
    }

我相信这会基于 this oracle doc:

The usage model for final fields is a simple one: Set the final fields for an object in that object's constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object's constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object's final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

不,这行不通。

final 不能像 volatile 那样保证线程之间的可见性。您引用的 Oracle 文档说其他线程将始终看到对象最终字段的 正确构造的 版本。 final 保证在对象构造函数完成时已构造和设置所有最终字段 运行。因此,如果对象 Foo 包含最终字段 bar,则 bar 保证在 Foo 的构造函数具有时被 构造 完成。

final 字段引用的对象仍然是可变的,并且对该对象的写入可能无法在不同线程中正确显示。

因此在您的示例中,其他线程不能保证看到已创建的 FieldHolder 对象并可能创建另一个对象,或者如果 FieldType 对象的状态发生任何修改,不保证其他线程会看到这些修改。 final 关键字只保证一旦其他线程确实看到 FieldType 对象,它的构造函数就会被调用。

引用@Kicsi提到的The "Double-Checked Locking is Broken" Declaration,最后一节是:

Double-Checked Locking Immutable Objects

If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic.

(重点是我的)

由于 FieldHolder 是不可变的,您确实不需要 volatile 关键字:其他线程将始终看到正确初始化的 FieldHolder。据我了解,FieldType 将始终在可以通过 FieldHolder.

从其他线程访问之前进行初始化

但是,如果 FieldType 不是不可变的,则适当的同步仍然是必要的。因此,我不确定避免使用 volatile 关键字会对您有多大好处。

如果它是不可变的,那么您根本不需要 FieldHolder,按照上面的引述。

首先要做的事情是:你试图做的事情充其量是危险的。当人们试图在决赛中作弊时,我有点紧张。 Java 语言为您提供 volatile 作为处理线程间一致性的首选工具。使用它。

总之,相关方法在 "Safe Publication and Initialization in Java" 为:

public class FinalWrapperFactory {
  private FinalWrapper wrapper;

  public Singleton get() {
    FinalWrapper w = wrapper;
    if (w == null) { // check 1
      synchronized(this) {
        w = wrapper;
        if (w == null) { // check2
          w = new FinalWrapper(new Singleton());
          wrapper = w;
        }
      }
    }
    return w.instance;
  }

  private static class FinalWrapper {
    public final Singleton instance;
    public FinalWrapper(Singleton instance) {
      this.instance = instance;
    }
  }
}

通俗点说,就是这样操作的。当我们观察到 wrapper 为 null 时,synchronized 产生正确的同步——换句话说,如果我们完全放弃第一个检查并将 synchronized 扩展到整个方法体,代码显然是正确的. FinalWrapper 中的 final 保证如果我们看到非空 wrapper,它是完全构建的,并且所有 Singleton 字段都是可见的——这从 [= 的活泼读取中恢复=13=].

请注意,它继承了字段中的 FinalWrapper,而不是值本身。如果 instance 在没有 FinalWrapper 的情况下发布,则所有赌注都将取消(用外行的话来说,这是过早的发布)。这就是为什么您的 Publisher.publish 功能失调:只是将值放入 final 字段,读回它,然后不安全地发布它是不安全的——这与将裸露的 instance 写出来非常相似。

此外,当您发现空 wrapper 并使用其值 时,您必须小心地在锁下进行 "fallback" 读取.在 return 语句中对 wrapper 进行第二次(第三次)读取也会破坏正确性,让您为合法比赛做好准备。

编辑:顺便说一下,如果你正在发布的对象在内部被 final-s 覆盖,你可以切断 FinalWrapper 的中间人,并发布instance本身。

编辑 2:另见 LCK10-J. Use a correct form of the double-checked locking idiom,以及评论中的一些讨论。

简而言之

没有 volatile 或包装器 class 的代码版本取决于 JVM 运行 所在的底层操作系统的内存模型。

带有包装器 class 的版本是一种已知的替代方案,称为 Initialization on Demand Holder 设计模式,它依赖于任何给定 class 加载的 ClassLoader 契约大多数一次,在第一次访问时,以线程安全的方式。

需要volatile

开发人员大多数时候想到代码执行的方式是将程序加载到主内存并直接从那里执行。然而,现实情况是在主存储器和处理器内核之间存在许多硬件高速缓存。出现问题是因为每个线程可能 运行 在不同的处理器上,每个处理器都有自己的 独立 范围内变量的副本;虽然我们喜欢从逻辑上将 field 视为一个位置,但实际情况要复杂得多。

为了运行通过一个简单的(虽然可能很冗长)示例,考虑一个具有两个线程和单级硬件缓存的场景,其中每个线程都有自己的 field 副本缓存。所以已经有 field 的三个版本:一个在主内存中,一个在第一个副本中,一个在第二个副本中。我将它们称为 fieldMfieldAfieldB分别.

  1. 初始状态
    fieldM = null
    fieldA = null
    fieldB = null
  2. 线程 A 执行第一次空检查,发现 fieldA 为空。
  3. 线程 A 获取 this 上的锁。
  4. 线程 B 执行第一次空检查,发现 fieldB 为空。
  5. 线程 B 试图获取 this 上的锁,但发现它被线程 A 持有。线程 B 休眠。
  6. 线程A执行第二次空检查,发现fieldA为空。
  7. 线程A给fieldA赋值fieldType1并释放锁。 因为 field 不是 volatile,所以这个赋值不会传播出去。
    fieldM = null
    fieldA = fieldType1
    fieldB = null
  8. 线程 B 唤醒并获取 this 上的锁。
  9. 线程 B 执行第二次空检查,发现 fieldB 为空。
  10. 线程B给fieldB赋值fieldType2并释放锁
    fieldM = null
    fieldA = fieldType1
    field B = fieldType2
  11. 在某些时候,对缓存副本 A 的写入会同步回主内存。
    fieldM = fieldType1
    fieldA = fieldType1
    fieldB = fieldType2
  12. 稍后,对缓存副本 B 的写入同步回主内存 覆盖副本 A 所做的分配。
    field M = fieldType2
    fieldA = fieldType1
    fieldB = fieldType2

作为上述问题的评论者之一,使用 volatile 确保写入可见。我不知道用于确保这一点的机制——可能是将更改传播到每个副本,也可能是从一开始就不会创建副本,并且 field 的所有访问都是针对 main内存。

关于此的最后一点说明:我之前提到过结果取决于系统。这是因为不同的底层系统可能对其内存模型采用不太乐观的方法,并将跨线程共享的 所有 内存视为 volatile 或者可能应用启发式方法来确定特定引用是否存在应该被视为 volatile 或不,尽管以同步到主内存的性能为代价。这会使测试这些问题成为一场噩梦;您不仅需要 运行 针对足够大的样本来尝试触发竞争条件,您可能恰好在一个足够保守的系统上进行测试,永远不会触发竞争条件。

按需初始化持有者

我想在这里指出的主要事情是,这是可行的,因为我们实际上是在将一个单身人士偷偷混入其中。 ClassLoader 契约意味着虽然可以有许多 Class 的实例,但对于任何类型 A 只能有一个 Class<A> 的实例可用,它也恰好被加载在第一次引用/延迟初始化时。事实上,您可以将 class 定义中的任何静态字段视为与 class 关联的单例中的字段,其中碰巧在该单例和实例之间增加了成员访问权限class.

使用 Enumnested static class helper 进行延迟初始化,否则如果初始化不会使用静态初始化花费很多成本(space 或时间)。

public enum EnumSingleton {
    /**
     * using enum indeed avoid reflection intruding but also limit the ability of the instance;
     */
    INSTANCE;

    SingletonTypeEnum getType() {
        return SingletonTypeEnum.ENUM;
    }
}

/**
 * Singleton:
 * The JLS guarantees that a class is only loaded when it's used for the first time
 * (making the singleton initialization lazy)
 *
 * Thread-safe:
 * class loading is thread-safe (making the getInstance() method thread-safe as well)
 *
 */
private static class SingletonHelper {
    private static final LazyInitializedSingleton INSTANCE = new LazyInitializedSingleton();
}

The "Double-Checked Locking is Broken" Declaration

With this change, the Double-Checked Locking idiom can be made to work by declaring the helper field to be volatile. This does not work under JDK4 and earlier.

  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }