双重检查锁定没有挥发性

Kic*_*csi 26 java multithreading final java-memory-model double-checked-locking

我看了这个问题,如何做双检锁:

// 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;
}
Run Code Online (Sandbox Code Playgroud)

我的目标是在没有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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

由于不需要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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用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;
    }
Run Code Online (Sandbox Code Playgroud)

或者甚至可能:

    // 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;
    }
Run Code Online (Sandbox Code Playgroud)

要么:

    // 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;
    }
Run Code Online (Sandbox Code Playgroud)

我相信这将基于这个oracle doc工作:

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

Ale*_*lev 28

首先要做的事情:你要做的事情充其量是危险的.当人们试图与决赛作弊时,我有点紧张.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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

外行人的条款,就像这样.synchronized当我们观察wrapper为null 时产生正确的同步- 换句话说,如果我们完全放弃第一个检查并扩展synchronized到整个方法体,代码显然是正确的.finalFinalWrapper保证中,如果我们看到非null wrapper,它是完全构造的,并且所有Singleton字段都是可见的 - 这从racy read中恢复wrapper.

请注意,它会FinalWrapper在字段中继承,而不是值本身.如果instance没有发布FinalWrapper,那么所有的投注都将被取消(以外行人的话说,这是过早的出版物).这就是为什么你Publisher.publish的功能不正常:只是把价值放在最后的领域,把它读回来,并且不安全地发布它并不安全 - 这与把赤裸裸的instance写出非常相似.

此外,你必须要小心做一个"回退"下锁阅读,当你发现空wrapper,并使用它的价值.对wrapperreturn语句进行第二次(第三次)阅读也会破坏正确性,为合法的种族做准备.

编辑:顺便说一下,如果您要发布的对象在final内部被-s 覆盖,那么您可以删除中间人FinalWrapper,并发布instance自己.

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


hay*_*ikh 6

简而言之

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

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

需要 volatile

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

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

  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分配field一个fieldType1,并释放锁. 因为field不是volatile这个赋值没有传播出去.
    fieldM = null
    fieldA = fieldType1
    fieldB =null
  8. 线程B唤醒并获得锁定this.
  9. 线程B执行第二次空检查,发现fieldB为空.
  10. 线程B分配fieldfieldType2并释放锁.
    fieldM = null
    fieldA = fieldType1
    fieldB =fieldType2
  11. 在某些时候,对高速缓存副本A的写入被同步回主存储器.
    fieldM = fieldType1
    fieldA = fieldType1
    fieldB =fieldType2
  12. 稍后,对高速缓存副本B的写入被同步回主存储器,覆盖由副本A进行的分配
    field.M = fieldType2
    fieldA = fieldType1
    fieldB =fieldType2

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

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

按需初始化持有人

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

  • 我怕你错了。在第 7 点 - 该字段确实可能不是易失性的,但释放锁保证了内存可见性,因此分配被传播出去并且线程 B 将意识到它。 (3认同)
  • @ZZ5 Volatile需要放入内存屏障,确保没有重新排序检查和引用集.由于volatile的语义不正确,因此在1.5之前的Java版本中的双重检查被特别打破.这是通过JSR 133在1.5中引入内存模型的一个激励因素.在该修复之后,双重检查锁定工作正常. (2认同)