我正在阅读文章双重检查锁定和单例模式,关于双重检查锁定如何被破坏,以及 Stack Overflow 上的一些相关问题。
我已经多次使用这种模式/习语,没有任何问题。自从我使用 Java 5 以来,我的第一个想法是 Java 5 内存模型已经纠正了这一点。然而文章说:
本文指的是 Java 5.0 修订前的 Java 内存模型;关于内存排序的陈述可能不再正确。但是,在新的内存模型下,双重检查锁定习语仍然被打破。
这是一个真正的问题吗?如果是,在什么条件下?
我正在阅读文章双重检查锁定和单例模式,关于双重检查锁定如何被破坏,以及 Stack Overflow 上的一些相关问题。
我已经多次使用这种模式/习语,没有任何问题。自从我使用 Java 5 以来,我的第一个想法是 Java 5 内存模型已经纠正了这一点。然而文章说:
本文指的是 Java 5.0 修订前的 Java 内存模型;关于内存排序的陈述可能不再正确。但是,在新的内存模型下,双重检查锁定习语仍然被打破。
这是一个真正的问题吗?如果是,在什么条件下?
同步块的开始保证您看到最新的数据,但它不保证重新排序,除非您也在同步块中,否则您不能期望数据的一致视图。它不保证在同步部分中完成的变量修改将对其他线程可见。只有进入同步块的线程才能保证看到更改。这就是双重检查锁定被破坏的原因 - 它在读者方面不同步。读取线程可能会看到,单例不为空,但单例数据可能未完全初始化(可见)。
另一方面,排序是由 volatile 提供的,它保证了排序,例如写入 volatile单例静态字段保证了对单例对象的写入将在写入 volatile 静态字段之前完成。它不会阻止创建两个对象的单例;这是由 synchronize 提供的。类 final 静态字段不需要是 volatile 的。在 Java 中,JVM 负责处理这个问题。
更多可以在以下位置找到:
有人很难确定他们的应用程序确实受到了双重检查锁故障的影响。事实上,许多使用此习惯用法的应用程序可能由于各种原因而永远不会遇到此问题。
但是,这并不意味着您应该使用它。存在不可量化的失败概率这一事实就足以说服您不要使用双重检查锁定,尤其是因为有安全的替代方案。
你只是走运了。
我们有一个应用程序使用了一个损坏的复查习语,它在很长一段时间内都能完美运行——不,事实上,我从来没有遇到过这个习语的问题。当然,无论如何我都修复了它。
我想其中一个原因是线程可见性最终将在现实世界中实现。一旦达到,它就会留下来。所以是的,很难检测到问题是否已经发生。
我相信部分依赖于这个事实的hashCode()
实现String
......线程在看不到缓存时计算hashCode,但最终他们开始看到。同时,重复计算只是浪费了一些CPU时间,避免 volatile 语义的记忆效应的好处胜过这种浪费的努力(至少这就是我猜他们以这种方式实现它的原因)。有效使用的成语是(实际的 String.hashCode() 实现):
/** Cache the hash code for the string */
private int hash; // Defaults to 0
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
显然,在使用它之前必须考虑和测量很多。