我正在学习Java并发,并且知道以下单例不是完全线程安全的。由于指令重新排序,线程可能在初始化之前获取实例。防止此潜在问题的正确方法是使用volatile关键字。
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance();
}
}
return instance;
}
}
我尝试在没有volatile关键字的情况下重现潜在问题,并编写了一个演示,表明使用上面的代码可能会在多线程环境中导致NullPointerException。但是我没有找到一种方法来明确地让Java编译器执行指令重新排序,并且我使用上面的单例的演示总是很好地运行而没有任何问题。
所以我的问题是如何显式启用/禁用Java编译器重新排序指令或如何在双重检查的锁定单例中使用volatile关键字来重现问题?
答案 0 :(得分:2)
这里危险的事情不一定,其他线程可能会从null
收到getInstance
作为答案。危险的是,他们可能会观察到一个尚未正确初始化的实例。
要检查这一点,请在您的单身人士中添加几个字段,例如:
class Singleton {
private List<Object> members;
private Singleton() {
members = new ArrayList<>();
members.addAll(queryMembers());
}
private Collection<Object> queryMembers() {
return Arrays.asList("Hello", 1, 2L, "world", new Object());
}
public int size() {
return members.size();
}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLocking.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
这被称为&#34;不安全的出版物&#34;。其他线程可能会看到单个实例部分初始化(即,members
字段可能仍为null
,或者列表可能为空,或者只是部分填充,或更糟:由于一个不一致的状态刚被添加的对象。)
在上面的示例代码中,size
的外部调用者不应该看到与5不同的值,对吧?我没有尝试过,但如果时间不对,呼叫者可以观察到不同的值,我不会感到惊讶。
原因是,允许编译器翻译
instance = new Singleton();
成为
的内容instance = allocate_instance(Singleton.class); // pseudo-code
instance.<init>();
因此,我们有一个窗口,其中instance
不再是null
,但实际对象尚未正确初始化。
The "Double-Checked Locking is Broken" Declaration对此进行了深入的解释。
答案 1 :(得分:0)
这是Java Concurrency in Practice书的摘录:
调试提示:对于服务器应用程序,请务必始终指定 -server JVM命令行在调用JVM时切换,甚至用于开发和测试。服务器JVM执行更多优化 比客户端JVM,例如从循环中提取变量 没有在循环中修改;可能似乎在 开发环境(客户端JVM)可以在部署中中断 环境(服务器JVM)。例如,如果我们“忘记”宣布 清单3.4中的变量asleep as volatile,服务器JVM可以 将测试从环路中提升(将其转换为无限循环),但是 客户端JVM不会。出现的无限循环 开发成本远远低于仅出现的开发成本 生产
所以你可以尝试一下。但是没有100%可靠的方法来启用重新排序。