双重检查锁定没有挥发性

问题描述 投票:23回答:5

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

我相信这将基于qazxsw poi:

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

java multithreading final java-memory-model double-checked-locking
5个回答
28
投票

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

无论如何,相关的方法在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; } } } 视为null时,synchronized产生正确的同步 - 换句话说,如果我们完全放弃第一个检查并将wrapper扩展到整个方法体,则代码显然是正确的。 synchronized中的final保证,如果我们看到非零FinalWrapper,它是完全构造的,并且所有wrapper字段都是可见的 - 这可以从Singleton的有趣读物中恢复。

请注意,它在字段中携带wrapper,而不是值本身。如果FinalWrapper在没有instance的情况下发布,那么所有的赌注都将被取消(以外行人的话说,这是过早的出版物)。这就是为什么你的FinalWrapper失去功能:只是把价值放在最后的领域,读回来,并且不安全地发布它并不安全 - 这非常类似于把赤身裸体的Publisher.publish写出来。

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

编辑:顺便说一下,如果您要发布的对象在内部被wrapper-s覆盖,那么你可能会切断final的中间人,并发布FinalWrapper本身。

编辑2:另见instance,以及那里的评论中的一些讨论。


6
投票

In short

没有LCK10-J. Use a correct form of the double-checked locking idiom或包装类的代码版本取决于运行JVM的底层操作系统的内存模型。

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

The need for ClassLoader

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

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

  1. 初始状态 fieldM = field nullA = field nullB = field
  2. 线程A执行第一次空检查,发现nullA为空。
  3. 线程A获取field上的锁定。
  4. 线程B执行第一次空检查,发现thisB为空。
  5. 线程B尝试获取field上的锁,但发现它由线程A保持。线程B休眠。
  6. 线程A执行第二次空检查,发现thisA为null。
  7. 线程A为fieldA分配值field并释放锁定。由于fieldType1不是field,因此这项任务不会传播出去。 volatileM = field nullA = field fieldType1B = field
  8. 线程B唤醒并获得null上的锁定。
  9. 线程B执行第二次空检查,发现thisB为空。
  10. 线程B为fieldB赋值field并释放锁。 fieldType2M = field nullA = field fieldType1B = field
  11. 在某些时候,对高速缓存副本A的写入被同步回主存储器。 fieldType2M = field fieldType1A = field fieldType1B = field
  12. 稍后,对高速缓存副本B的写入被同步回主存储器,覆盖由副本A进行的分配。 fieldType2M = field fieldType2A = field fieldType1B = field

作为提到的问题的评论者之一,使用fieldType2确保写入是可见的。我不知道用于确保这一点的机制 - 可能是更改传播到每个副本,可能是副本永远不会在第一个位置进行,并且volatile的所有访问都是针对主内存的。

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

Initialization on Demand holder

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


2
投票

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

双重检查锁定不可变对象

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

(重点是我的)

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

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

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


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

通过这种改变,可以通过声明辅助字段是易失性来使双重锁定的习语起作用。这在JDK4及更早版本下不起作用。

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

-1
投票

不,这不行。

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.