使用易失性
这是一个线程关心另一个线程在做什么的情况吗?那么JMM FAQ有答案:
大多数时候,一个线程并不关心另一个线程在做什么。但是当它发生时,这就是同步的目的。
作为对那些说 OP 的代码是安全的人的回应,请考虑一下: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)
)。即使 setter 的调用者继续更新映射“h”,这也会生成一个不会更改的快照。例如,他们可能会清除地图并添加新条目。
依赖接口
List
从风格上讲,通常最好使用像and这样的抽象类型来声明 API Map
,而不是像ArrayList
andHashMap.
这样的具体类型。如果具体类型需要更改(就像我在这里所做的那样),这在未来提供了灵活性。
缓存
将“h”分配给“validProgramCodes”的结果可能只是对处理器缓存的写入。即使新线程启动,“h”对新线程也不会可见,除非它已被刷新到共享内存。一个好的运行时会避免刷新,除非它是必要的,而 usingvolatile
是表明它是必要的一种方式。
重新排序
假设以下代码:
HashMap codes = new HashMap();
codes.putAll(source);
meta.setValidProgramCodes(codes);
如果setValidCodes
只是 OP's 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”。Iterator 底层的映射经历了并发修改,并抛出了运行时异常——一个极其间歇的、看似莫名其妙的运行时异常,从来没有测试过程中产生。
并发编程
任何时候你有一个线程关心另一个线程在做什么,你必须有某种内存屏障来确保一个线程的操作对另一个线程是可见的。如果一个线程中的事件必须在另一个线程中的事件之前发生,则必须明确指出。否则无法保证。在实践中,这意味着volatile
或synchronized
。
不要吝啬。不正确的程序无法完成其工作的速度并不重要。此处显示的示例简单且做作,但请放心,它们说明了现实世界的并发错误,由于其不可预测性和平台敏感性,这些错误非常难以识别和解决。
其他资源