双重检查锁定没有挥发性

时间:2015-04-26 20:54:21

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

我读了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

  

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

5 个答案:

答案 0 :(得分:27)

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

无论如何,相关的方法在中描述 "Safe Publication and Initialization in Java" as:

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

外行人的条款,就像这样。当我们将synchronized视为null时,wrapper会产生正确的同步 - 换句话说,如果我们完全放弃第一个检查并将synchronized扩展到整个方法体,则代码显然是正确的。 final中的FinalWrapper保证,如果我们看到非空wrapper,它是完全构造的,并且所有Singleton字段都是可见的 - 这可以从对{ {1}}。

请注意,它会覆盖字段中的wrapper,而不是值本身。如果FinalWrapper在没有instance的情况下发布,则所有投注都将被取消(通俗地说,这是过早的出版物)。这就是为什么你的FinalWrapper失去功能的原因:只是将值放在最后一个字段中,将其读回来并且不安全地发布它并不安全 - 这与仅仅将裸Publisher.publish写出来非常相似。 / p>

此外,当您发现null instance并使用其值时,您必须小心在锁定下进行“后备”读取。在return语句中对wrapper进行第二次(第三次)读取也会破坏正确性,为合法竞赛做好准备。

编辑:顺便说一下,如果您要发布的对象在内部被wrapper - 覆盖,那么您可以删除final的中间人,并发布{{ 1}}本身。

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

答案 1 :(得分:6)

简而言之

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

带有包装类的版本是一种已知的替代方法,称为Initialization on Demand Holder 设计模式,并且依赖于ClassLoader契约,任何给定的类最多一次加载,第一次访问时,以及线程安全的方式。

需要volatile

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

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

  1. 初始状态
    field M = null
    field A = {{1} } null B = field
  2. 线程A执行第一次空检查,查找null A 为空。
  3. 主题A获取field上的锁定。
  4. 线程B执行第一次空检查,查找this B 为空。
  5. 线程B尝试获取field上的锁,但发现线程A持有它。线程B休眠。
  6. 线程A执行第二次空检查,找到this A 为空。
  7. 线程A为field A 赋值field并释放锁。 由于fieldType1不是field,因此不会传播此作业。
    volatile M = field
    null A = field
    fieldType1 B = field
  8. 线程B唤醒并获取null上的锁定。
  9. 线程B执行第二次空检查,查找this B 为空。
  10. 主题B指定field B field并释放锁。
    fieldType2 M = { {1}} field A = null
    field B = fieldType1
  11. 在某些时候,对缓存副本A的写入将同步回主内存。
    field M = fieldType2
    {{1 } A = field
    fieldType1 B = field
  12. 稍后,对缓存副本B的写入将同步回主内存,覆盖副本A所做的分配。
    fieldType1 M = field
    fieldType2 A = field
    fieldType2 B = { {1}}
  13. 作为上述问题的评论者之一,使用field可确保写入可见。我不知道用于确保这一点的机制 - 可能是更改传播到每个副本,可能是副本永远不会被创建,fieldType1的所有访问都是反对主要记忆。

    关于此的最后一点:我之前提到过,结果取决于系统。这是因为不同的底层系统可能对其内存模型采取不太乐观的方法,并将所有内存在线程之间共享的内容视为field,或者可能应用启发式方法来确定是否应该处理特定引用尽管是fieldType2,但代价是与主内存同步的性能。这可以使这些问题的测试成为一场噩梦;你不仅要对足够大的样本进行操作以试图触发竞争条件,你可能恰好在一个足够保守的系统上进行测试,从而不会触发这种情况。

    按需持有者初始化

    我想在这里指出的主要事情是,这是有效的,因为我们基本上将单身人士偷偷摸摸地混合在一起。 volatile合约意味着虽然field有很多实例,但只有volatile个实例可用于任何类型volatile,也恰好加载在第一次参考/懒惰初始化时。实际上,您可以将类定义中的任何静态字段视为与该类关联的单例中的字段,其中恰好在该单例与该类的实例之间增加了成员访问权限。

答案 2 :(得分:2)

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

  

双重检查锁定不可变对象

     

如果Helper是一个不可变对象,那么所有的字段都是   帮助者是最终的,然后双重检查锁定将无需工作   使用volatile字段。这个想法是对不可变的引用   对象(如String或Integer)的行为应该大致相同   作为int或float的方式;读写不可变的引用   对象是原子的。

(重点是我的)

由于FieldHolder是不可变的,因此您确实不需要volatile关键字:其他线程将始终看到正确初始化的FieldHolder。据我所知,FieldType将始终在通过FieldHolder从其他线程访问之前进行初始化。

但是,如果FieldType不是不可变的,则仍然需要正确的同步。因此,我不确定避免使用volatile关键字会带来多大好处。

如果它是不可改变的,那么根据上述报价,你根本不需要FieldHolder

答案 3 :(得分:0)

使用 Enum 嵌套静态类帮助器进行延迟初始化,否则,如果初始化不会花费太多成本(空间或时间),请仅使用静态初始化。

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

  

通过此更改,可以通过声明helper字段是可变的来使Double-Checked Locking惯用语起作用。在JDK4和更早的版本中,这不起作用。

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

答案 4 :(得分:-1)

不,这不行。

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

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

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