双重检查锁定没有波动

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

// 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 publish(T val){ return new Publish(val).get(); } private static class Publish{ private final T val; public Publish(T val) { this.val = val; } public T get(){ return val; } } } 

由于不需要volatile,因此可能会更快地访问时间,同时仍然保持可重用Publisher类的简单性。


我用jcstress测试了这个。 SafeDCLFinal按预期工作,而UnsafeDCLFinal不一致(如预期的那样)。 在这一点上我99%肯定它的工作,但请,certificate我错了。 使用mvn clean install -pl tests-custom -am并使用java -XX:-UseCompressedOops -jar tests-custom/target/jcstress.jar -t DCLFinal运行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 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 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 publish(T val, boolean safe){ if(safe){ return new SafePublish(val).get(); } return new UnsafePublish(val).get(); } private static class UnsafePublish{ T val; public UnsafePublish(T val) { this.val = val; } public T get(){ return val; } } private static class SafePublish{ 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字段的用法模型很简单:在该对象的构造函数中设置对象的最终字段; 并且在对象的构造函数完成之前,不要在另一个线程可以看到的地方写入对正在构造的对象的引用。 如果遵循此操作,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本 。 它还将看到那些最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样是最新的。

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

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

请注意,它在字段中inheritance了FinalWrapper ,而不是值本身。 如果在不使用FinalWrapper instance发布instance ,则所有投注都将被取消( FinalWrapper话说,这是过早的发布)。 这就是你的Publisher.publish失效的原因:只是将值放在final字段中,将其读回并且不安全地发布它是不安全的 – 这与将裸instance写出来非常相似。

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

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

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

简而言之

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

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

需要volatile

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

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

  1. 初始状态
    field M = null
    field A = null
    field B = null
  2. 线程A执行第一次空检查,查找field A为空。
  3. 线程A获取此锁定。
  4. 线程B执行第一次空检查,查找field B为空。
  5. 线程B尝试获取此锁,但发现它由线程A保持。线程Bhibernate。
  6. 线程A执行第二次空检查,查找field A为空。
  7. 线程A为field A指定值fieldType1并释放锁定。 由于field不是volatile因此不会传播该分配。
    field M = null
    field A = fieldType1
    field B = null
  8. 线程B唤醒并获取锁定。
  9. 线程B执行第二次空检查,查找field B为空。
  10. 线程B为field B指定值fieldType2并释放锁定。
    field M = null
    field A = fieldType1
    field B = fieldType2
  11. 在某些时候,对高速缓存副本A的写入被同步回主存储器。
    field M = fieldType1
    field A = fieldType1
    field B = fieldType2
  12. 稍后,对高速缓存副本B的写入被同步回主存储器, 覆盖由副本A进行的分配。
    field M = fieldType2
    field A = fieldType1
    field B = fieldType2

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

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

按需初始化持有者

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

引用@Kicsi提到的“Double-checked Locking is Broken”声明 ,最后一部分是:

双重检查锁定不可变对象

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

(重点是我的)

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

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

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

不,这不行。

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

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

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