这是交易.我有一个哈希映射,包含我称之为"程序代码"的数据,它存在于一个对象中,如下所示:
Class Metadata { private HashMap validProgramCodes; public HashMap getValidProgramCodes() { return validProgramCodes; } public void setValidProgramCodes(HashMap h) { validProgramCodes = h; } }
我有很多很多的读取器线程,每个读取器线程都会调用getValidProgramCodes()一次,然后将该hashmap用作只读资源.
到现在为止还挺好.这是我们感兴趣的地方.
我想放入一个计时器,每个计时器都会生成一个新的有效程序代码列表(不管怎么做),并调用setValidProgramCodes.
我的理论 - 我需要帮助验证 - 是我可以继续按原样使用代码,而不需要进行显式同步.它是这样的:在更新validProgramCodes时,validProgramCodes的值总是很好 - 它是指向新的或旧的hashmap的指针. 这是一切都取决于的假设. 拥有旧hashmap的读者可以; 他可以继续使用旧值,因为它不会被垃圾收集,直到他释放它.每个读者都是暂时的; 它很快就会消亡,并被一个将获得新价值的新人所取代.
这有水吗?我的主要目标是在绝大多数没有更新的情况下避免代价高昂的同步和阻塞.我们每小时只更新一次,读者不断闪烁.
这是一个线程关心另一个人正在做什么的情况吗?然后JMM常见问题答案得到答案:
大多数时候,一个线程并不关心对方在做什么.但是当它发生时,这就是同步的目的.
为了回应那些说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",这也会使快照不会改变.例如,他们可能会清除地图并添加新条目.
关于风格音符,它往往是更好地与抽象类型,如声明的API List
和Map
,而不是一个具体的类型,如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);
假设在执行writer 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
.
不要吝啬.不正确的程序无法完成其工作的速度并不重要.这里显示的示例很简单,但是可以肯定的是,它们说明了由于其不可预测性和平台敏感性而难以识别和解决的真实并发错误.
Java语言规范 - 17个线程和锁定部分:§17.3和§17.4
JMM FAQ
Doug Lea的并发书籍