29

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

// 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+。查看文档


但我想知道这是否可行:

    // 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 doc工作:

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

4

5 回答 5

30

首先要做的事情是:您尝试做的事情充其量是危险的。当人们试图在决赛中作弊时,我有点紧张。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;
    }
  }
}

外行的话,它是这样工作的。synchronized当我们观察wrapper为 null 时产生正确的同步——换句话说,如果我们完全放弃第一次检查并扩展synchronized到整个方法体,代码显然是正确的。保证如果我们看到非 null final,它是完全构造的,并且所有字段都是可见的——这从.FinalWrapperwrapperSingletonwrapper

请注意,它会继承FinalWrapper字段中的 ,而不是值本身。如果instance没有FinalWrapper. 这就是为什么你Publisher.publish的功能失调:只是将值放入 final 字段,读回它,然后不安全地发布它是不安全的——这与将裸instance写输出非常相似。

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

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

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

于 2015-05-05T09:09:38.100 回答
9

简而言之

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

带有包装类的版本是一种已知的替代方案,称为按需初始化设计模式,它依赖于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
    fieldB =fieldType2
  11. 在某些时候,对缓存副本 A 的写入会同步回主内存。
    fieldM = fieldType1
    fieldA = fieldType1
    fieldB =fieldType2
  12. 稍后,对缓存副本 B 的写入会同步回主存,覆盖副本 A 所做的分配。
    fieldM = fieldType2
    fieldA = fieldType1
    fieldB =fieldType2

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

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

按需初始化持有人

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

于 2015-05-07T03:30:08.547 回答
2

引用@Kicsi 提到的“双重检查锁定被破坏”声明,最后一部分是:

双重检查锁定不可变对象

如果 Helper 是一个不可变对象,使得 Helper 的所有字段都是最终的,那么双重检查锁定将起作用,而无需使用 volatile fields。这个想法是对不可变对象(如字符串或整数)的引用应该与 int 或 float 的行为方式大致相同;对不可变对象的读写引用是原子的。

(重点是我的)

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

FieldType但是,如果不是不可变的,则仍然需要适当的同步。因此,我不确定您是否会从避免使用该volatile关键字中受益匪浅。

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

于 2015-04-30T15:25:53.353 回答
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();
}

“双重检查锁定被破坏”声明

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

  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }
于 2019-03-28T03:50:29.440 回答
-1

不,这行不通。

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

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

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

于 2015-04-26T21:48:02.873 回答