Java双重检查locking
我碰巧遇到了一篇最近讨论Java中的双重检查locking模式的文章,以及它的陷阱,现在我想知道如果我已经使用多年的模式的变体现在受到任何问题。
我已经看了很多关于这个主题的post和文章,并且了解了获得对部分构build的对象的引用的潜在问题,据我所知,我认为我的实现不受这些问题的影响。 以下模式有问题吗?
而且,如果不是,人们为什么不使用它? 在这个问题上我看过的任何讨论都没有看到过。
public class Test { private static Test instance; private static boolean initialized = false; public static Test getInstance() { if (!initialized) { synchronized (Test.class) { if (!initialized) { instance = new Test(); initialized = true; } } } return instance; } }
双重检查locking被破坏 。 由于初始化是一个原语,因此它可能不需要易失性工作,但是在初始化实例之前,没有什么能够防止初始化被认为对于非同步代码是真的。
编辑:澄清上面的答案,原来的问题问到关于使用布尔值来控制双重检查locking。 没有上述链接的解决scheme,它将无法正常工作。 你可以仔细检查lock实际上是否设置了一个布尔值,但是在创build类实例时仍然存在关于指令重新sorting的问题。 build议的解决scheme不起作用,因为在非同步化块中将初始化布尔值视为true后,实例可能未初始化。
正确的解决方法是使用volatile(在实例字段上)并忘记初始化的布尔值,并且确保使用JDK 1.5或更高版本,或者在最终字段中初始化它,如链接中所阐述的文章和汤姆的回答,或者只是不使用它。
当然,整个概念似乎是一个巨大的不成熟的优化,除非你知道你会得到这个Singleton的线程争夺吨,或者你已经对应用程序进行了剖析,并已经看到这是一个热点。
这将工作,如果initialized
是volatile
。 正如synchronized
的volatile
有趣的影响,真的不是那么多的参考,作为我们可以说的其他数据。 设置instance
字段和Test
对象被强制发生 – 在写入之前进行initialized
。 当通过短路使用caching的值时,在读取instance
和通过引用到达的对象之前 , 发生 initialize
读取。 有一个单独的initialized
标志没有显着差异(除了会导致代码更复杂)。
(不安全发布的构造函数中的final
字段的规则有些不同。)
但是,在这种情况下,您应该很less看到这个错误。 第一次使用时遇到麻烦的可能性很小,而且是不重复的比赛。
代码过于复杂。 你可以把它写成:
private static final Test instance = new Test(); public static Test getInstance() { return instance; }
双重检查locking的确被打破了,问题的解决scheme实际上比这个习惯用法更简单 – 只需要使用一个静态初始化器即可。
public class Test { private static final Test instance = createInstance(); private static Test createInstance() { // construction logic goes here... return new Test(); } public static Test getInstance() { return instance; } }
一个静态初始化器保证在JVM加载类的第一次被执行,并且在类引用可以被返回到任何线程之前 – 使得它本质上是线程安全的。
这是双重检查locking被打破的原因。
同步保证,只有一个线程可以input一个代码块。 但是并不能保证在同步部分内完成的variables修改对其他线程是可见的。 只有进入同步块的线程才能看到更改。 这就是为什么双重检查locking被破坏的原因 – 它在读者一侧是不同步的。 读线程可能会看到,单例不是null,但单例数据可能没有完全初始化(可见)。
订购由volatile
提供。 volatile
保证顺序,例如写入volatilevariables的singleton静态字段保证写入singleton对象的写操作在写入volatilevariables之前完成。 它不会阻止创build两个对象的单例,这是通过同步提供的。
类最终静态字段不需要是易失性的。 在Java中, JVM负责处理这个问题。
看到我的文章, 一个单例模式的答案和在真实世界的Java应用程序中的双重检查locking的破裂 ,说明了一个关于双重检查locking的单例的例子,看起来很聪明但被破坏。
您应该使用java.util.concurrent.atomic中的primefaces数据types。
如果“初始化”为真,则“实例”必须完全初始化,与1加1等于2 :)相同。 因此,代码是正确的。 该实例只是实例化一次,但该函数可能被称为一百万次,所以它提高了性能,而不检查同步一百万减一次。
还有一些情况可能会使用双重检查。
- 首先,如果你真的不需要一个单身人士,并且使用双重检查只是为了不创build和初始化许多对象。
- 在构造函数/初始化块的末尾设置了
final
字段(这会导致所有以前初始化的字段被其他线程看到)。
我一直在研究双重locking的成语,从我的理解,你的代码可能会导致阅读部分构造的实例的问题,除非你的testing类是不可变的:
Java内存模型为共享不可变对象提供了初始化安全的特殊保证。
即使不使用同步发布对象引用,也可以安全地访问它们。
(来自非常可取的书Java并发实践的引文)
所以在这种情况下,双重检查的locking方式将起作用。
但是,如果情况并非如此,请注意您正在返回variables实例而没有同步,因此实例variables可能不会完全构造(您将看到属性的默认值而不是构造函数中提供的值)。
布尔variables不会添加任何东西来避免这个问题,因为它可能在Test类被初始化之前设置为true(synchronized关键字不能避免完全重新sorting,有些sencences可能会改变顺序)。 Java存储模型中没有发生规则来保证这一点。
而使布尔variables不会添加任何东西,因为32位variables是以primefaces方式在Java中创build的。 双重locking的习惯用法也适用于他们。
从Java 5开始,您可以修复将实例variables声明为volatile的问题。
在这个非常有趣的文章中,你可以阅读更多关于双重检查的习惯用法。
最后,我读过一些build议:
-
考虑如果你应该使用单身模式。 这被很多人认为是反模式。 在可能的情况下,dependency injection是首选。 检查这个 。
-
在仔细考虑之前,仔细考虑双重locking优化是否真的是必要的,因为在大多数情况下,这是不值得的。 另外,考虑在静态字段中构造Test类,因为延迟加载只有在构造一个类需要大量资源时才有用,在大多数情况下,情况并非如此。
如果你仍然需要执行这个优化,请检查这个链接 ,它提供了一些替代scheme,以达到与你正在尝试的效果类似的效果。
虽然DCL问题似乎在许多虚拟机上运行,但是DCL问题仍然存在。 这里有一个很好的关于这个问题的文章http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html 。
multithreading和内存一致性是比它们看起来更复杂的主题。 […]如果你只是使用Java提供的工具来完成这个目的,那么你可以忽略所有这些复杂性 – 同步。 如果将每个访问同步到可能已被写入或可能被另一个线程读取的variables,则不会有内存一致性问题。
正确地解决这个问题的唯一方法是避免懒惰的初始化(急切地执行)或单个检查一个同步块内部。 initialized
布尔值的使用相当于引用本身的空检查。 第二个线程可能会看到initialized
为true,但instance
可能仍然为空或部分初始化。
双选locking是反模式。
延迟初始化持有者类是您应该查看的模式。
尽pipe还有很多其他的答案,我想我应该回答,因为还没有一个简单的答案,说明为什么DCL在许多情况下被打破,为什么没有必要,你应该做什么。 所以我将使用Goetz: Java Concurrency In Practice
的一句话Goetz: Java Concurrency In Practice
,它在Java Memory模型的最后一章中提供了最简单的解释。
这是关于variables的安全公布:
DCL的真正问题是这样的假设:读取共享对象引用而不同步时可能发生的最糟糕的事情是错误地看到陈旧的值(在这种情况下为null); 在这种情况下,DCL习惯用法通过重新尝试locking来弥补这一风险。 但是最糟糕的情况实际上却相当严重 – 可以看到参考的当前值,但是可以看到对象状态的陈旧值,这意味着可以看到对象处于无效或不正确的状态。
JMM(Java 5.0和更高版本)中的后续更改使DCL能够在资源变得易失性时工作,并且由于易失性读取通常只比非易失性读取稍贵,所以性能影响较小。
然而,这是一个习惯用语大部分已经过去的习惯用法,激励它的力量(缓慢无竞争的同步,慢JVM启动)不再起作用,使得它作为一个优化不太有效。 懒惰的初始化持有者惯用法提供了相同的好处,并且更容易理解。
代码清单16.6 懒惰初始化持有者类成语。
public class ResourceFactory private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } }
这是做到这一点的方法。
首先,对于单身人士,你可以使用一个枚举,在这个问题中解释用一个枚举实现单身人士(Java)
其次,从Java 1.5开始,您可以使用带有双重检查locking的volatilevariables,如本文末尾所述: https : //www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html