【问题标题】:Double-checked locking without volatile没有 volatile 的双重检查锁定
【发布时间】:2015-04-26 20:54:20
【问题描述】:

我阅读了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 类的简单性。


我使用 jcstress 对此进行了测试。 SafeDCLFinal 按预期工作,而 UnsafeDCLFinal 不一致(如预期)。在这一点上,我 99% 确定它有效,但请证明我错了。使用mvn clean install -pl tests-custom -am 编译并使用java -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+。 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

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

【问题讨论】:

  • 就算成功了,又有什么好处呢?
  • @isnot2bad 省略 volatile 可能会更快地访问该字段。
  • 如果没有volatile,它将无法工作。这都是关于可见性,而不是原子性。如果field 不是volatile,则一个线程的field 的写入操作可能永远不会对另一个线程的读取操作可见。摆弄是没有意义的——线程安全的单例已经解决了。
  • @isnot2bad 我要提到double checked locking is a broken system(渴望初始化> 懒惰),但他没有提到这是一个单身人士
  • @Kicsi 在您的情况下,外部field 不是final

标签: java multithreading final java-memory-model double-checked-locking


【解决方案1】:

首先要做的事情是:您尝试做的事情充其量是危险的。当人们试图在决赛中作弊时,我有点紧张。 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 为空时,synchronized 产生了正确的同步——换句话说,如果我们完全放弃第一次检查并将synchronized 扩展到整个方法体,那么代码显然是正确的。 final in FinalWrapper 保证如果我们看到非空的 wrapper,它是完全构造的,并且所有 Singleton 字段都是可见的——这从 wrapper 的活泼读取中恢复。

请注意,它会继承字段中的FinalWrapper,而不是值本身。如果在没有FinalWrapper 的情况下发布instance,那么所有的赌注都将失败(用外行的话来说,这是过早的发布)。这就是为什么您的 Publisher.publish 无法正常工作的原因:仅将值放入 final 字段,读回它,然后不安全地发布它是不安全的——这与将 instance 裸写出来非常相似。

此外,当您发现空的wrapper并使用它的值时,您必须小心在锁下进行“回退”读取。在返回语句中对wrapper 进行第二次(第三次)读取也会破坏正确性,从而为您的合法比赛做好准备。

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

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

【讨论】:

【解决方案2】:

总之

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

带有包装类的版本是一种已知的替代方案,称为Initialization on Demand Holder 设计模式,它依赖于ClassLoader 协定,即任何给定的类在首次访问时以线程安全的方式最多加载一次.

需要volatile

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

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

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

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

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

Initialization on Demand 持有者

我想在这里指出的主要一点是,它之所以有效,是因为我们实际上是在混音中混入一个单例。 ClassLoader 合约意味着虽然Class 可以有很多实例,但对于任何类型A,只能有一个Class&lt;A&gt; 实例可用,这也恰好在第一次引用时首先加载/懒惰-初始化。实际上,您可以将类定义中的任何静态字段视为与该类相关联的单例中的字段,其中恰好在该单例和该类的实例之间增加了成员访问权限。

【讨论】:

  • 恐怕你错了。在第 7 点中 - 该字段确实可能不是易失性的,但释放锁保证了内存可见性,因此分配被传播出去并且线程 B 会知道它。
  • @ZZ5 : 是否意味着 DCL 中不需要 volatile?
  • @mav3n 是的,在 DCL 中不需要它。一般来说,DCL 本身是一个坏主意,它是在 Java 的早期版本中引入的,因为即使是无竞争的同步访问也有点昂贵,但事实并非如此。有关更多信息,请参阅 Java 并发实践中的专用 DCL 一章
  • @ZZ5 Volatile 需要放入内存屏障,确保检查和引用集没有重新排序。由于 volatile 的语义不正确,1.5 之前的 Java 版本中的双重检查锁定被破坏。这是通过 JSR 133 在 1.5 中引入内存模型的一个激励因素。在修复之后,双重检查锁定工作正常。
【解决方案3】:

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

双重检查锁定不可变对象

如果 Helper 是一个不可变对象,那么 助手是最终的,然后双重检查锁定将在没有 使用可变字段。这个想法是对不可变的引用 对象(例如字符串或整数)的行为应该大致相同 作为 int 或 float 的方式;对不可变对象的读写引用 对象是原子的。

(重点是我的)

由于FieldHolder 是不可变的,您确实不需要volatile 关键字:其他线程将始终看到正确初始化的FieldHolder。据我了解,FieldType 将始终被初始化,然后才能通过FieldHolder 从其他线程访问。

但是,如果FieldType 不是不可变的,则仍然需要正确同步。因此,我不确定您是否会从避免使用 volatile 关键字中获得很多好处。

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

【讨论】:

  • FieldType 不一定是不可变的(它可能包含非最终字段),尽管我们可以假设它在构造后不会更改,或者如果确实更改了,那么该更改的线程安全性不是我们关心的问题.
  • 你是对的,FieldType 的不变性可能不会影响你的解决方案的有效性。我相应地更新了我的答案。
  • 请注意,链接的文章仅适用于 1.5 之前的 Java 版本;正如文章本身所指出的那样,Java 1.5 及以后的内存模型修复了所描述的行为。
  • @hayden.sikh 链接的文章有一个针对 1.5 及更高版本的特定部分,引用的文本来自该部分。
【解决方案4】:

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

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

通过此更改,可以通过将辅助字段声明为易失性来使双重检查锁定习惯用法起作用。这在 JDK4 及更早版本下不起作用。

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

【讨论】:

    【解决方案5】:

    不,这行不通。

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

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

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

    【讨论】:

    • “虽然最终字段引用的对象仍然是可变的,但对该对象的写入可能无法在不同线程中正确可见。” - 该对象稍后不会被修改(编辑我的问题)“其他线程不能保证看到已创建的 FieldHolder 对象并可能创建另一个” - 同步处理该问题
    • 如果您不打算修改它,那么它应该可以工作,并且您不需要使用 final 关键字的包装类,因为我认为它不会比仅仅实例化 FieldType 获得任何好处同步块,因为包装类不是(也不能是)最终的。
    • 请阅读Double-Checked Locking is Broken - 如果不将“字段”设置为易失性,它不会简单地工作。
    • 你是对的。发表此评论后不久,我意识到这一点。对此感到抱歉。
    猜你喜欢
    • 1970-01-01
    • 2015-03-10
    • 2011-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多