Java 双重检查锁定
我最近偶然发现一篇文章讨论了 Java 中的双重检查锁定模式及其陷阱,现在我想知道我多年来一直使用的该模式的变体是否存在任何问题。
我已经查看了有关该主题的许多帖子和文章,并了解获取对部分构造的对象的引用的潜在问题,据我所知,我不认为我的实现是受这些问题的影响。下面的模式有什么问题吗?
如果没有,为什么人们不使用它?我从未在我见过的有关此问题的任何讨论中看到推荐它。
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;
}
}
I happened upon an article recently discussing the double checked locking pattern in Java and its pitfalls and now I'm wondering if a variant of that pattern that I've been using for years now is subject to any issues.
I've looked at many posts and articles on the subject and understand the potential issues with getting a reference to a partially constructed object, and as far as I can tell, I don't think my implementation is subject to these issues. Are there any issues with the following pattern?
And, if not, why don't people use it? I've never seen it recommended in any of the discussion I've seen around this issue.
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;
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
双重检查锁定已损坏。由于initialized是一个原语,它可能不需要它是易失性的才能工作,但是没有什么可以阻止在初始化实例之前将initialized视为非同步代码的真实值。
编辑:为了澄清上述答案,最初的问题询问使用布尔值来控制双重检查锁定。如果没有上面链接中的解决方案,它将无法工作。您可以仔细检查实际设置布尔值的锁定,但在创建类实例时仍然存在指令重新排序的问题。建议的解决方案不起作用,因为在非同步块中看到初始化的布尔值为 true 后,实例可能无法初始化。
双重检查锁定的正确解决方案是使用 易失性(在实例字段上)并忘记初始化的布尔值,并确保使用 JDK 1.5 或更高版本,或者在最终字段中初始化它,如链接中所述文章和汤姆的回答,或者只是不使用它。
当然,整个概念看起来像是一个巨大的过早优化,除非您知道在获取此单例时会遇到大量线程争用,或者您已经分析了应用程序并发现这是一个热点。
Double check locking is broken. Since initialized is a primitive, it may not require it to be volatile to work, however nothing prevents initialized being seen as true to the non-syncronized code before instance is initialized.
EDIT: To clarify the above answer, the original question asked about using a boolean to control the double check locking. Without the solutions in the link above, it will not work. You could double check lock actually setting a boolean, but you still have issues about instruction reordering when it comes to creating the class instance. The suggested solution does not work because instance may not be initialized after you see the initialized boolean as true in the non-syncronized block.
The proper solution to double-check locking is to either use volatile (on the instance field) and forget about the initialized boolean, and be sure to be using JDK 1.5 or greater, or initialize it in a final field, as elaborated in the linked article and Tom's answer, or just don't use it.
Certainly the whole concept seems like a huge premature optimization unless you know you are going to get a ton of thread contention on getting this Singleton, or you have profiled the application and have seen this to be a hot spot.
如果
initialized
是易失性
,那么就可以工作。就像synchronized
一样,volatile
的有趣效果实际上与引用无关,而与我们对其他数据的影响无关。instance
字段和Test
对象的设置被强制发生在写入初始化
之前。通过短路使用缓存值时,initialize
读取发生在读取实例
以及通过引用到达的对象之前。拥有单独的initialized
标志没有显着差异(除了它会导致代码更加复杂之外)。(构造函数中用于不安全发布的
final
字段的规则略有不同。)但是,在这种情况下您应该很少会看到该错误。第一次使用时遇到麻烦的机会微乎其微,而且是一场不重复的比赛。
代码过于复杂。你可以把它写成:
That would work if
initialized
wasvolatile
. Just as withsynchronized
the interesting effects ofvolatile
are not really so much to do with the reference as what we can say about other data. Setting up of theinstance
field and theTest
object is forced to happen-before the write toinitialized
. When using the cached value through the short circuit, theinitialize
read happens-before reading ofinstance
and objects reached through the reference. There is no significant difference in having a separateinitialized
flag (other than it causes even more complexity in the code).(The rules for
final
fields in constructors for unsafe publication are a little different.)However, you should rarely see the bug in this case. The chances of getting into trouble when using for the first time is minimal, and it is a non-repeated race.
The code is over-complicated. You could just write it as:
双重检查锁定确实被破坏了,并且该问题的解决方案实际上比这种习惯更容易以代码方式实现 - 只需使用静态初始值设定项。
静态初始化程序保证在 JVM 第一次加载类时以及类引用返回到任何线程之前执行 - 使其本质上是线程安全的。
Double checked locking is indeed broken, and the solution to the problem is actually simpler to implement code-wise than this idiom - just use a static initializer.
A static initializer is guaranteed to be executed the first time that the JVM loads the class, and before the class reference can be returned to any thread - making it inherently threadsafe.
这就是双重检查锁定被破坏的原因。
同步保证只有一个线程可以进入代码块。但它不能保证在同步部分中完成的变量修改对其他线程可见。只有进入同步块的线程才能保证看到更改。这就是双重检查锁定被破坏的原因 - 它在读者端不同步。读取线程可能会看到单例不为空,但单例数据可能未完全初始化(可见)。
排序由
易失性
提供。易失性
保证顺序,例如写入易失性单例静态字段保证对单例对象的写入将在写入易失性静态字段之前完成。它不会阻止创建两个对象的单例,这是由同步提供的。类最终静态字段不需要是易失性的。在 Java 中,JVM 解决了这个问题。
请参阅我的帖子 对现实世界 Java 应用程序中的单例模式和损坏的双重检查锁定的回答,说明了关于双重检查锁定的单例示例,该示例看起来很聪明,但已损坏。
This is the reason why double checked locking is broken.
Synchronize guarantees, that only one thread can enter a block of code. But it doesn't guarantee, that variables modifications done within synchronized section will be visible to other threads. Only the threads that enters the synchronized block is guaranteed to see the changes. This is the reason why double checked locking is broken - it is not synchronized on the reader's side. The reading thread may see, that the singleton is not null, but singleton data may not be fully initialized (visible).
Ordering is provided by
volatile
.volatile
guarantees ordering, for instance write to volatile singleton static field guarantees that writes to the singleton object will be finished before the write to volatile static field. It doesn't prevent creating singleton of two objects, this is provided by synchronize.Class final static fields doesn't need to be volatile. In Java, the JVM takes care of this problem.
See my post, an answer to Singleton pattern and broken double checked locking in a real-world Java application, illustrating an example of a singleton with respect to double-checked locking that looks clever but is broken.
双重检查锁定是反模式。
延迟初始化持有者类是您应该查看的模式。
尽管有这么多其他答案,我认为我应该回答,因为仍然没有一个简单的答案可以说明为什么 DCL 在许多情况下被破坏,为什么它是不必要的以及你应该做什么。因此,我将引用 Goetz:Java 并发实践中的一句话,对我来说,它在有关 Java 内存模型的最后一章中提供了最简洁的解释。
这是关于变量的安全发布:
这就是实现它的方法。
Double-checked Locking is the anti-pattern.
Lazy Initialization Holder Class is the pattern you should be looking at.
Despite so many other answers, I figured I should answer because there still isn't one simple answer that says why DCL is broken in many contexts, why it is unnecessary and what you should do instead. So I'll use a quote from
Goetz: Java Concurrency In Practice
which for me provides the most succint explanation in its final chapter on the Java Memory Model.It's about Safe Publication of variables:
That's the way to do it.
您可能应该使用 java.util.concurrent.atomic。
You should probably use the atomic data types in java.util.concurrent.atomic.
如果“initialized”为 true,则“instance”必须完全初始化,与 1 加 1 等于 2 相同:)。因此,该代码是正确的。该实例仅实例化一次,但该函数可能被调用一百万次,因此它确实提高了性能,而无需检查一百万次同步。
If "initialized" is true, then "instance" MUST be fully initialized, same as 1 plus 1 equals 2 :). Therefore, the code is correct. The instance is only instantiated once but the function may be called a million times so it does improve the performance without checking synchronization for a million minus one times.
我一直在研究双重检查锁定习惯用法,根据我的理解,您的代码可能会导致读取部分构造的实例的问题,除非您的测试类是不可变的:
(引自非常值得推荐的《Java Concurrency in Practice》一书)
因此,在这种情况下,双重检查锁定惯用法将起作用。
但是,如果情况并非如此,请注意您在没有同步的情况下返回变量实例,因此实例变量可能未完全构造(您将看到属性的默认值而不是构造函数中提供的值)。
boolean变量没有添加任何东西来避免这个问题,因为它可能在Test类初始化之前被设置为true(synchronized关键字不能完全避免重新排序,某些语句可能会改变顺序)。 Java 内存模型中没有“happens-before”规则来保证这一点。
并且使布尔值成为 volatility 也不会添加任何内容,因为 32 位变量是在 Java 中自动创建的。双重检查锁定习惯也适用于它们。
从 Java 5 开始,您可以通过将实例变量声明为易失性来解决该问题。
您可以在这篇非常有趣的文章<中阅读有关双重检查惯用语的更多信息< /a>.
最后,我读过一些建议:
考虑是否应该使用单例模式。许多人认为这是一种反模式。如果可能的话,首选依赖注入。检查这个。
在实现双重检查锁定优化之前,请仔细考虑是否确实有必要,因为在大多数情况下,这是不值得的。另外,请考虑在静态字段中构造 Test 类,因为延迟加载仅在构造类需要大量资源时才有用,而在大多数情况下,情况并非如此。
如果您仍然需要执行此优化,请检查此 链接 它提供了一些替代方案来实现与您正在尝试的效果类似的效果。
I've been investigating about the double checked locking idiom and from what I understood, your code could lead to the problem of reading a partially constructed instance UNLESS your Test class is immutable:
(Quotations from the very advisable book Java Concurrency in Practice)
So in that case, the double checked locking idiom would work.
But, if that is not the case, observe that you are returning the variable instance without synchronization, so the instance variable may not be completely constructed (you would see the default values of the attributes instead of the values provided in the constructor).
The boolean variable doesn't add anything to avoid the problem, because it may be set to true before the Test class is initialized (the synchronized keyword doesn't avoid reordering completely, some sencences may change the order). There is no happens-before rule in the Java Memory Model to guarantee that.
And making the boolean volatile wouldn't add anything either, because 32 bits variables are created atomically in Java. The double checked locking idiom would work with them as well.
Since Java 5, you can fix that problem declaring the instance variable as volatile.
You can read more about the double checked idiom in this very interesting article.
Finally, some recommendations I've read:
Consider if you should use the singleton pattern. It is considered an anti-pattern by many people. Dependency Injection is preferred where possible. Check this.
Consider carefully if the double checked locking optimization is really necessary before implementing it, because in most cases, that wouldn't be worth the effort. Also, consider constructing the Test class in the static field, because lazy loading is only useful when constructing a class takes a lot of resources and in most of the times, it is not the case.
If you still need to perform this optimization, check this link which provides some alternatives for achieving a similar effect to what you are trying.
DCL 问题已被解决,尽管它似乎适用于许多虚拟机。这里有一篇关于这个问题的很好的文章 http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html。
正确解决此问题的唯一方法是避免惰性初始化(急切地执行)或在同步块内进行单一检查。使用布尔
initialized
相当于对引用本身进行空检查。第二个线程可能会看到initialized
为 true,但instance
可能仍为 null 或部分初始化。The DCL problem is broken, even though it seems to works on many VMs. There is a nice writeup about the problem here http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html.
The only way to solve this problem properly is to avoid lazy initialization (do it eagerly) or to single check inside a synchronized block. The use of the boolean
initialized
is equivalent to a null check on the reference itself. A second thread may seeinitialized
being true butinstance
might still be null or partially initialized.首先,对于单例,您可以使用枚举,如本问题 Implementing Singleton with枚举(Java 中)
其次,从 Java 1.5 开始,您可以使用具有双重检查锁定的 volatile 变量,如本文末尾所述:https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
First, for singletons you can use an Enum, as explained in this question Implementing Singleton with an Enum (in Java)
Second, since Java 1.5, you can use a volatile variable with double checked locking, as explained at the end of this article: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
在某些情况下仍然可以使用双重检查。
final
字段(这导致其他线程可以看到所有先前初始化的字段)。There are still some cases when a double check may be used.
final
field set at the end of the constructor/initialized block (that causes all previously initialized fields to be seen by other threads).