使用易失性
这是一个线程关心另一个线程在做什么的情况吗?那么JMM常见问题解答就有了答案:
大多数时候,一个线程并不关心另一个线程在做什么。但是,当它这样做时,这就是同步的目的。
为了回应那些说OP的代码是安全的,请考虑一下:Java的内存模型中没有任何东西可以保证当启动新线程时,该字段将被刷新到主内存中。此外,只要在线程中无法检测到更改,JVM 就可以自由地对操作进行重新排序。
从理论上讲,读取器线程不能保证看到“写入”有效的程序代码。在实践中,他们最终会,但你不能确定什么时候。
我建议将有效的程序代码成员声明为“易失性”。速度差异可以忽略不计,并且无论引入什么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));
}
}
不变性
除了用 包装它之外,我还复制了地图()。这使得快照不会更改,即使 setter 的调用方继续更新映射“h”。例如,他们可能会清除地图并添加新条目。unmodifiableMap
new HashMap(h)
取决于接口
在风格上,通常最好使用抽象类型(如 and )来声明 API,而不是像 and 这样的具体类型,如果具体类型需要更改,这将在将来提供灵活性(就像我在这里所做的那样)。List
Map
ArrayList
HashMap.
缓存
将“h”分配给“validProgramCodes”的结果可能只是写入处理器的缓存。即使新线程启动,“h”对新线程也不可见,除非它已被刷新到共享内存。除非有必要,否则良好的运行时将避免刷新,而使用是指示有必要的一种方法。volatile
重组
假定以下代码:
HashMap codes = new HashMap();
codes.putAll(source);
meta.setValidProgramCodes(codes);
如果只是OP的,编译器可以自由地对代码进行如下重新排序:setValidCodes
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: // Do something with e.
6: }
现在假设编写器线程在读者的第 2 行和第 3 行之间的映射上调用“putAll”。迭代器基础的映射经历了并发修改,并引发了运行时异常 — 一个魔鬼般的间歇性、看似难以解释的运行时异常,在测试期间从未产生过。
并发编程
每当一个线程关心另一个线程正在做什么时,您必须具有某种内存屏障,以确保一个线程的操作对另一个线程可见。如果一个线程中的事件必须发生在另一个线程中的事件之前,则必须显式指示这一点。除此之外,没有其他保证。实际上,这意味着 或 。volatile
synchronized
不要吝啬。无论不正确的程序无法完成其工作的速度有多快。这里显示的示例简单而人为,但请放心,它们说明了现实世界的并发错误,由于其不可预测性和平台敏感性,这些错误非常难以识别和解决。
其他资源