双重检查锁定,无易失性总之需要volatile 按需初始化持有者

我读了这个关于如何进行双重检查锁定的问题:

// 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;
        }
    }
}

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


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

/*
 * 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 +一起使用。查看文档


但我想知道这是否有效:

    // 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;
    }

我相信这将基于这个oracle文档工作

最终字段的使用模型很简单:在对象的构造函数中设置对象的最终字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到它的位置写入对正在构造的对象的引用。如果遵循此命令,则当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到由最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样最新。


答案 1

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

无论如何,相关方法在“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;
    }
  }
}

这是外行的术语,它的工作原理是这样的。 当我们观察为 null 时,会产生正确的同步 - 换句话说,如果我们完全删除第一个检查并扩展到整个方法体,则代码显然是正确的。 在保证中,我们看到非空值,它是完全构造的,并且所有字段都是可见的 - 这从的不规则读取中恢复。synchronizedwrappersynchronizedfinalFinalWrapperwrapperSingletonwrapper

请注意,它继承了 in 字段,而不是值本身。如果在没有的情况下发布,所有的赌注都将被取消(用外行的话说,这是过早的出版)。这就是为什么你是不起作用的:只是把值放在最后一个字段中,读回去,然后不安全地发布它是不安全的 - 这与只是把裸写的写出来非常相似。FinalWrapperinstanceFinalWrapperPublisher.publishinstance

另外,当您发现 null 时,您必须小心地在锁下进行“回退”读取,并使用其值。做第二次(第三次)阅读回车声明也会破坏正确性,让你为合法的比赛做好准备。wrapperwrapper

编辑:顺便说一句,整个事情都说,如果你发布的对象在内部被-s覆盖,你可以削减 的中间人,并发布它本身。finalFinalWrapperinstance

编辑2:另见LCK10-J。使用正确形式的双重检查锁定习语,并在评论中进行一些讨论。


答案 2

总之

不带 包装类 或 包装类的代码版本取决于运行 JVM 的基础操作系统的内存模型。volatile

带有包装类的版本是一种已知的替代方法,称为按需初始化持有者设计模式,它依赖于这样一个协定,即任何给定类在首次访问时以线程安全的方式最多加载一次。ClassLoader

需要volatile

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

若要运行一个简单的(尽管可能是详细的)示例,请考虑一个具有两个线程和一个硬件缓存级别的方案,其中每个线程在该缓存中都有自己的副本。所以已经有三个版本:一个在主内存中,一个在第一个副本中,一个在第二个副本中。我将分别称为MABfieldfieldfieldfieldfield

  1. 初始状态
    M =
    A =
    Bfieldnullfieldnullfield = null
  2. 线程 A 执行第一个空值检查,发现 A 为空。field
  3. 线程 A 获取 上的锁。this
  4. 线程 B 执行第一个空值检查,发现 B 为空。field
  5. 线程 B 尝试获取锁定打开,但发现它由线程 A 持有。线程 B 处于睡眠状态。this
  6. 线程 A 执行第二次空值检查,发现 A 为空。field
  7. 线程 A 为 A 赋值并释放锁。由于字段不是易失性的,因此不会传播此分配。
    M =
    A =
    BfieldfieldType1fieldnullfieldfieldType1field = null
  8. 线程 B 唤醒并获取 上的锁。this
  9. 线程 B 执行第二次空值检查,发现 B 为空。field
  10. 线程 B 为 B 分配值并释放锁。
    M =
    A =
    BfieldfieldType2fieldnullfieldfieldType1field = fieldType2
  11. 在某些时候,对缓存副本 A 的写入操作会同步回主内存。
    M =
    A =
    BfieldfieldType1fieldfieldType1field = fieldType2
  12. 稍后,对缓存副本 B 的写入将同步回主内存,从而覆盖副本 A 所做的分配。
    M =
    A =
    BfieldfieldType2fieldfieldType1field = fieldType2

作为所提问题的评论者之一,使用可确保写入是可见的。我不知道用于确保这一点的机制 - 可能是更改被传播到每个副本,可能是副本从一开始就没有制作过,并且所有访问都是针对主内存的。volatilefield

最后要注意的是:我之前提到过,结果是系统相关的。这是因为不同的底层系统可能会对其内存模型采取不太乐观的方法,并将线程之间共享的所有内存视为或可能应用启发式方法来确定是否应将特定引用视为不确定,尽管代价是与主内存同步的性能。这可能使这些问题的测试成为一场噩梦;您不仅必须针对足够大的样本运行以尝试触发争用条件,而且您可能只是碰巧在足够保守的系统上进行测试,永远不会触发该条件。volatilevolatile

按需初始化持有者

我想在这里指出的主要事情是,这是有效的,因为我们本质上是将单例偷偷带入混合中。合约意味着虽然可以有许多 实例,但任何类型的实例都只能有一个实例,当第一次引用/懒惰初始化时,它也恰好在第一个加载。实际上,您可以将类定义中的任何静态字段视为与该类关联的单例中的字段,其中该单例与该类的实例之间恰好增加了成员访问权限。ClassLoaderClassClass<A>A