使用Volatile
这是一个一个线程关心另一个线程正在做什么的情况吗?那么JMM FAQ就有了答案:
大多数情况下,一个线程不关心另一个线程正在做什么。但是当它关心时,就需要使用同步机制。
对于那些认为原始代码已经安全的人,可以考虑以下情况:Java内存模型中没有任何东西能够保证该字段在新线程启动时被刷新到主存储器中。此外,JVM可以自由地重新排序操作,只要更改在线程内不可检测即可。
理论上,读取线程不能保证看到validProgramCodes的“写入”。实际上,他们最终会看到,但不能确定何时。
我建议将validProgramCodes成员声明为“volatile”。速度差异将是微不足道的,并且它将确保您的代码现在和将来的安全性,无论引入了什么JVM优化。
以下是具体的建议:
import java.util.Collections;
class Metadata {
private volatile Map validProgramCodes = Collections.emptyMap();
public Map getValidProgramCodes() {
return validProgramCodes;
}
public void setValidProgramCodes(Map h) {
if (h == null)
throw new NullPointerException("validProgramCodes == null");
validProgramCodes = Collections.unmodifiableMap(new HashMap(h));
}
}
不可变性
除了使用unmodifiableMap
进行包装之外,我还会复制地图(new HashMap(h)
)。这将创建一个快照,即使设置器的调用者继续更新地图"h",它也不会改变。例如,他们可能会清除地图并添加新条目。
依赖于接口
从风格上讲,通常最好使用抽象类型(如List
和Map
)声明API,而不是具体类型(如ArrayList
和HashMap
)。如果需要更改具体类型(就像我在这里做的那样),这样可以提供灵活性。
缓存
将"h"分配给"validProgramCodes"的结果可能只是对处理器缓存的一次写入。即使启动了新线程,除非已将"h"刷新到共享内存,否则新线程将看不到它。一个好的运行时会避免刷新,除非必要,并且使用volatile
是一种指示它必要的方法之一。
重新排序
假设以下代码:
HashMap codes = new HashMap()
codes.putAll(source)
meta.setValidProgramCodes(codes)
如果setValidCodes
仅仅是OP的validProgramCodes = h;
,编译器可以自行重排序代码,类似于这样:
1: meta.validProgramCodes = codes = new HashMap()
2: codes.putAll(source)
假设在执行第1行写入指令后,一个读取线程开始运行以下代码:
1: Map codes = meta.getValidProgramCodes();
2: Iterator i = codes.entrySet().iterator();
3: while (i.hasNext()) {
4: Map.Entry e = (Map.Entry) i.next();
5:
6: }
现假设写线程在读取器的第2行和第3行之间在地图上调用了“putAll”。迭代器底层的地图已经发生了并发修改,并抛出运行时异常——一种魔鬼般的间歇性、似乎无法解释的运行时异常,在测试期间从未出现过。
并发编程
任何时候,如果一个线程关心另一个线程正在做什么,你必须有某种内存屏障来确保一个线程的操作对另一个线程是可见的。如果一个线程中的事件必须在另一个线程中发生之前发生,你必须明确指示。否则就没有任何保证。实践中,这意味着使用volatile
或synchronized
。
不要省略这个步骤。一个错误的程序多快都不能完成它的工作。这里展示的例子是简单而人为的,但请放心,它们说明了非常难以识别和解决的真实世界并发错误,因为它们的不可预测性和平台敏感性。
其他资源