Java双重检查锁定
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1625118/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java double checked locking
提问by
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.
我最近看到一篇文章,讨论了 Java 中的双重检查锁定模式及其陷阱,现在我想知道我多年来一直使用的那种模式的变体是否会出现任何问题。
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 thinkmy 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;
}
}
回答by Tom Hawtin - tackline
That would work if initialized
was volatile
. Just as with synchronized
the interesting effects of volatile
are not really so much to do with the reference as what we can say about other data. Setting up of the instance
field and the Test
object is forced to happen-beforethe write to initialized
. When using the cached value through the short circuit, the initialize
read happens-beforereading of instance
and objects reached through the reference. There is no significant difference in having a separate initialized
flag (other than it causes even more complexity in the code).
如果initialized
是volatile
. 就像synchronized
有趣的效果volatile
一样,与我们可以谈论的其他数据相比,与参考无关。建立的instance
领域和Test
对象是被迫发生,之前的写操作initialized
。当通过短路使用缓存值时,initialize
读取发生在读取instance
和对象通过引用到达之前。拥有单独的initialized
标志没有显着差异(除了它会导致代码更加复杂)。
(The rules for final
fields in constructors for unsafe publication are a little different.)
(final
不安全发布的构造函数中的字段规则略有不同。)
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:
代码过于复杂。你可以把它写成:
private static final Test instance = new Test();
public static Test getInstance() {
return instance;
}
回答by Yishai
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.
编辑:为了澄清上述答案,最初的问题询问了使用布尔值来控制双重检查锁定。如果没有上面链接中的解决方案,它将无法工作。您可以仔细检查 lock 实际设置一个布尔值,但在创建类实例时仍然存在指令重新排序的问题。建议的解决方案不起作用,因为在非同步块中看到初始化的布尔值为 true 后,实例可能不会被初始化。
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.
双重检查锁定的正确解决方案是使用 volatile(在实例字段上)并忘记初始化的布尔值,并确保使用 JDK 1.5 或更高版本,或者在最终字段中初始化它,如链接中所述文章和汤姆的回答,或者干脆不使用它。
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.
当然,整个概念似乎是一个巨大的过早优化,除非您知道在获得这个 Singleton 时会遇到大量线程争用,或者您已经对应用程序进行了概要分析并且已经看到这是一个热点。
回答by matt b
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.
双重检查锁定确实被破坏了,并且该问题的解决方案实际上比这个习惯用法在代码方面实现更简单 - 只需使用静态初始化程序。
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;
}
}
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.
静态初始化器保证在 JVM 首次加载类时执行,并且在类引用可以返回到任何线程之前执行 - 使其具有固有的线程安全性。
回答by Seun Osewa
You should probably use the atomic data types in java.util.concurrent.atomic.
您可能应该使用java.util.concurrent.atomic 中的原子数据类型。
回答by Sudhakar Kalmari
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.
订购由 提供volatile
。volatile
保证排序,例如写入 volatile 单例静态字段保证写入单例对象将在写入 volatile 静态字段之前完成。它不会阻止创建两个对象的单例,这是由同步提供的。
Class final static fields doesn't need to be volatile. In Java, the JVMtakes care of this problem.
类 final 静态字段不需要是可变的。在 Java 中,JVM负责处理这个问题。
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.
请参阅我的帖子,对实际 Java 应用程序中的单例模式和损坏的双重检查锁定的回答,说明了一个关于双重检查锁定的单例示例,该示例看起来很聪明但已损坏。
回答by Hyman
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.
如果“已初始化”为真,则“实例”必须完全初始化,与 1 加 1 等于 2 相同:)。因此,代码是正确的。该实例仅被实例化一次,但该函数可能会被调用一百万次,因此它确实提高了性能,而无需检查同步一百万减一倍。
回答by user590444
There are still some cases when a double check may be used.
在某些情况下,可能会使用双重检查。
- First, if you really don't need a singleton, and double check is used just for NOT creating and initializing to many objects.
- There is a
final
field set at the end of the constructor/initialized block (that causes all previously initialized fields to be seen by other threads).
- 首先,如果您真的不需要单例,则使用双重检查仅用于不创建和初始化多个对象。
final
在构造函数/初始化块的末尾设置了一个字段(这会导致其他线程看到所有先前初始化的字段)。
回答by Jorge
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:
我一直在研究双重检查锁定习语,据我所知,除非您的 Test 类是不可变的,否则您的代码可能会导致读取部分构造实例的问题:
The Java Memory Model offers a special guarantee of initialization safety for sharing immutable objects.
They can be safely accessed even when synchronization is not used to publish the object reference.
Java 内存模型为共享不可变对象提供了初始化安全的特殊保证。
即使不使用同步来发布对象引用,也可以安全地访问它们。
(Quotations from the very advisable book Java Concurrency in Practice)
(引自非常值得推荐的《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.
boolean 变量没有添加任何东西来避免这个问题,因为它可能在 Test 类初始化之前设置为 true(synchronized 关键字并没有完全避免重新排序,某些句子可能会改变顺序)。Java Memory Model 中没有happens-before 规则来保证这一点。
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.
并且使布尔值 volatile 也不会添加任何内容,因为在 Java 中以原子方式创建了 32 位变量。双重检查锁定习语也适用于它们。
Since Java 5, you can fix that problem declaring the instance variable as volatile.
从 Java 5 开始,您可以通过将实例变量声明为 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.
考虑是否应该使用单例模式。许多人认为这是一种反模式。在可能的情况下,首选依赖注入。检查这个。
在实施之前仔细考虑双重检查锁定优化是否真的必要,因为在大多数情况下,这不值得付出努力。另外,考虑在静态字段中构造Test类,因为延迟加载仅在构造类需要大量资源时才有用,而在大多数情况下,情况并非如此。
If you still need to perform this optimization, check this linkwhich provides some alternatives for achieving a similar effect to what you are trying.
回答by Adam
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.
DCL 问题已解决,尽管它似乎适用于许多 VM。这里有一篇关于这个问题的好文章http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html。
multithreading and memory coherency are more complicated subjects than they might appear. [...] You can ignore all of this complexity if you just use the tool that Java provides for exactly this purpose -- synchronization. If you synchronize every access to a variable that might have been written, or could be read by, another thread, you will have no memory coherency problems.
多线程和内存一致性是比它们看起来更复杂的主题。[...] 如果您只是使用 Java 提供的工具——同步,那么您可以忽略所有这些复杂性。如果您同步对可能已写入或可由另一个线程读取的变量的每次访问,则不会出现内存一致性问题。
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 see initialized
being true but instance
might still be null or partially initialized.
正确解决此问题的唯一方法是避免延迟初始化(急切地执行)或在同步块内进行单项检查。布尔值的使用initialized
等效于对引用本身的空检查。第二个线程可能会看到initialized
为真,但instance
可能仍为空或部分初始化。
回答by Adam
Double-checked Lockingis the anti-pattern.
双重检查锁定是反模式。
Lazy Initialization Holder Classis the pattern you should be looking at.
Lazy Initialization Holder Class是您应该关注的模式。
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.
尽管有很多其他答案,但我认为我应该回答,因为仍然没有一个简单的答案可以说明为什么 DCL 在许多情况下被破坏,为什么它没有必要以及您应该做什么。因此,我将引用一段引文,Goetz: Java Concurrency In Practice
在它关于 Java 内存模型的最后一章中对我提供了最简洁的解释。
It's about Safe Publication of variables:
这是关于变量的安全发布:
The real problem with DCL is the assumption that the worst thing that can happen when reading a shared object reference without synchronization is to erroneously see a stale value (in this case, null ); in that case the DCL idiom compensates for this risk by trying again with the lock held. But the worst case is actually considerably worse—it is possible to see a current value of the reference but stale values for the object's state, meaning that the object could be seen to be in an invalid or incorrect state.
Subsequent changes in the JMM (Java 5.0 and later) have enabled DCL to work if resource is made volatile , and the performance impact of this is small since volatile reads are usually only slightly more expensive than nonvolatile reads.
However, this is an idiom whose utility has largely passed—the forces that motivated it (slow uncontended synchronization, slow JVM startup) are no longer in play, making it less effective as an optimization. The lazy initialization holder idiom offers the same benefits and is easier to understand.
Listing 16.6. Lazy Initialization Holder Class Idiom.
public class ResourceFactory private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } }
DCL 的真正问题是假设在不同步的情况下读取共享对象引用时可能发生的最糟糕的事情是错误地看到过时的值(在本例中为 null );在这种情况下,DCL 习惯用法通过在持有锁的情况下再次尝试来补偿这种风险。但最坏的情况实际上要糟糕得多——可能会看到引用的当前值但对象状态的值已经过时,这意味着可能会看到对象处于无效或不正确的状态。
JMM(Java 5.0 及更高版本)中的后续更改使 DCL 能够在资源设置为 volatile 时工作,并且这对性能的影响很小,因为 volatile 读取通常仅比非易失性读取稍微贵一点。
然而,这是一个习语,其效用在很大程度上已经过去了——驱动它的力量(缓慢的无竞争同步、缓慢的 JVM 启动)不再起作用,使其作为优化的效果降低。惰性初始化持有者习语提供了相同的好处并且更容易理解。
清单 16.6。延迟初始化持有者类习语。
public class ResourceFactory private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } }
That's the way to do it.
这就是这样做的方法。