public class Factory {
private Singleton instance;
public Singleton getInstance() {
Singleton res = instance;
if (res == null) {
synchronized (this) {
res = instance;
if (res == null) {
res = new Singleton();
instance = res;
}
}
}
return res;
}
}
Run Code Online (Sandbox Code Playgroud)
它几乎是正确的线程安全实现Singleton
.我看到的唯一问题是:
将thread #1
被初始化的instance
字段可以发布之前就被完全初始化.现在,第二个线程可以读取instance
不一致的状态.
但是,就我而言,这只是问题所在.这只是问题吗?(而且我们可以变得instance
不稳定).
您可以通过安全发布中的Shipilev 和Java中的安全初始化来解释您的示例.我强烈建议阅读整篇文章,但总结一下,请看UnsafeLocalDCLFactory
那里的部分:
public class UnsafeLocalDCLFactory implements Factory {
private Singleton instance; // deliberately non-volatile
@Override
public Singleton getInstance() {
Singleton res = instance;
if (res == null) {
synchronized (this) {
res = instance;
if (res == null) {
res = new Singleton();
instance = res;
}
}
}
return res;
}
}
Run Code Online (Sandbox Code Playgroud)
以上有以下问题:
这里引入局部变量是一个正确性修复,但只是部分:在发布Singleton实例和读取其任何字段之前仍然没有发生过.我们只是保护自己不返回"null"而不是Singleton实例.同样的技巧也可以被视为SafeDCLFactory的性能优化,即只进行一次易失性读取,产生:
Shipilev建议通过标记instance
volatile 来解决如下问题:
public class SafeLocalDCLFactory implements Factory {
private volatile Singleton instance;
@Override
public Singleton getInstance() {
Singleton res = instance;
if (res == null) {
synchronized (this) {
res = instance;
if (res == null) {
res = new Singleton();
instance = res;
}
}
}
return res;
}
}
Run Code Online (Sandbox Code Playgroud)
这个例子没有其他问题.
编辑我在这里又写了一个答案,应该可以消除所有的困惑。
这是一个很好的问题,我将在这里尝试总结一下我的理解。
假设Thread1
当前正在初始化Singleton
实例并发布引用(显然不安全)。Thread2
可以看到这个不安全的已发布引用(意味着它看到一个非空引用),但这并不意味着它通过该引用看到的字段(Singleton
通过构造函数初始化的字段)也被正确初始化。
据我所知,发生这种情况是因为构造函数内部可能会发生字段存储的重新排序。由于没有“发生在”规则(这些是普通变量),所以这完全有可能。
但这并不是唯一的问题。请注意,您在这里执行了两次读取:
if (res == null) { // read 1
return res // read 2
Run Code Online (Sandbox Code Playgroud)
这些读取没有同步保护,因此这些是恶意读取。AFAIK 这意味着允许读取 1 读取非空引用,而读取 2 允许读取空引用。
顺便说一句,这与所有强大的 Shipilev 解释的都是一样的(即使我 1/2 年读过一次这篇文章,我每次仍然会发现一些新东西)。
确实,举例volatile
可以解决问题。当你让它变得不稳定时,会发生这种情况:
instance = res; // volatile write, thus [LoadStore][StoreStore] barriers
Run Code Online (Sandbox Code Playgroud)
所有“其他”操作(来自构造函数内的存储)都无法通过此栅栏,因此不会重新排序。这也意味着当您读取 volatile 变量并看到非空值时,这意味着在写入 volatile 本身之前完成的每个“写入”都肯定发生了。这篇优秀的文章有它的确切含义
这也解决了第二个问题,因为这些操作不能重新排序,所以您可以保证从read 1
和看到相同的值read 2
。
无论我读了多少书并试图理解这些事情对我来说总是很复杂,我认识的人很少有人能够编写这样的代码并正确地推理它。如果可以的话(我愿意!)请坚持使用双重检查锁定的已知和有效示例:)
归档时间: |
|
查看次数: |
334 次 |
最近记录: |