Java并发场景 – 我是否需要同步?

这是交易。 我有一个哈希映射,包含我称之为“程序代码”的数据,它存在于一个对象中,如下所示:

Class Metadata { private HashMap validProgramCodes; public HashMap getValidProgramCodes() { return validProgramCodes; } public void setValidProgramCodes(HashMap h) { validProgramCodes = h; } } 

我有很多很多的读取器线程,每个读取器线程都会调用getValidProgramCodes()一次,然后将该hashmap用作只读资源。

到现在为止还挺好。 这是我们感兴趣的地方。

我想放入一个计时器,每个计时器都会生成一个新的有效程序代码列表(不管怎么做),并调用setValidProgramCodes。

我的理论 – 我需要帮助validation – 是我可以继续按原样使用代码,而无需进行显式同步。 它是这样的:在更新validProgramCodes时,validProgramCodes的值总是很好 – 它是指向新的或旧的hashmap的指针。 这是一切都取决于的假设。 拥有旧hashmap的读者可以; 他可以继续使用旧值,因为在释放之前它不会被垃圾收集。 每个读者都是短暂的; 它很快就会消亡,并被一个能够获得新价值的新人所取代。

这有水吗? 我的主要目标是在绝大多数没有更新的情况下避免代价高昂的同步和阻塞。 我们每小时只更新一次,读者不断闪烁。

使用易失性

这是一个线程关心另一个人正在做什么的情况吗? 然后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”,这也会使快照不会改变。 例如,他们可能会清除地图并添加新的条目。

取决于接口

在风格上,使用ListMap等抽象类型声明API通常更好,而不是像ArrayListHashMap.这样的具体类型HashMap. 如果具体类型需要改变(如我在这里所做的那样),这将在未来提供灵活性。

高速缓存

将“h”分配给“validProgramCodes”的结果可能只是写入处理器的高速缓存。 即使新线程启动,新线程也不会看到“h”,除非它已被刷新到共享内存。 一个好的运行时将避免刷新,除非它是必要的,并且使用volatile是一种表明它是必要的方法。

重新排序

假设以下代码:

 HashMap codes = new HashMap(); codes.putAll(source); meta.setValidProgramCodes(codes); 

如果validProgramCodes = h;只是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”。迭代器底层的映射经历了并发修改,并抛出了一个运行时exception – 一个exception间歇性的,看似无法解释的运行时exception,从来没有在测试期间产生。

并发编程

每当你有一个线程关心另一个线程正在做什么时,你必须有某种内存屏障,以确保一个线程的操作对另一个线程可见。 如果一个线程中的事件必须在另一个线程中的事件之前发生,则必须明确指出。 除此之外没有任何保证。 在实践中,这意味着volatilesynchronized

不要吝啬。 不正确的程序无法完成其工作的速度并不重要。 这里显示的示例很简单,但是可以肯定的是,它们说明了由于其不可预测性和平台敏感性而难以识别和解决的真实并发错误。

其他资源

  • Java语言规范 – 17个线程和锁定部分: §17.3和§17.4
  • JMM FAQ
  • Doug Lea的并发书籍

不,代码示例不安全,因为没有安全发布任何新的HashMap实例。 没有任何同步,读者线程可能会看到部分初始化的 HashMap。

在他的回答中查看@ erickson在“重新排序”下的解释。 另外,我不能推荐Brian Goetz的书“ Java Concurrency in Practice” !

阅读器线程是否可以看到旧的(陈旧的)HashMap引用,或者甚至可能永远不会看到新引用,这是否合适。 可能发生的最糟糕的事情是读者线程可能获得对尚未初始化且未准备好访问的HashMap实例的引用并尝试访问。

不,通过Java内存模型(JMM),这不是线程安全的。

在编写和读取HashMap实现对象之间没有发生过 – 之前的关系。 因此,虽然编写器线程似乎首先写出对象然后写入引用,但读者线程可能看不到相同的顺序。

如前所述,无法保证reaer线程能够看到新值。 在实际使用现有硬件上的当前编译器时,值应该更新,除非循环体足够小以至于可以充分内联。

因此,在新的JMM下,使参考volatile变得足够。 它不太可能对系统性能产生重大影响。

这个故事的寓意:线程很难。 不要试图聪明,因为有时候(可能不在你的测试系统上)你不够聪明。

正如其他人已经指出的那样,这不安全,你不应该这样做。 您需要volatile或synchronized来强制其他线程查看更改。

没有提到的是同步和特别易变的可能比你想象的要快得多。 如果它实际上是您应用中的性能瓶颈,那么我会吃掉这个网页。

另一个选项(可能比volatile更慢,但YMMV)是使用ReentrantReadWriteLock来保护访问,以便多个并发读者可以读取它。 如果这仍然是性能瓶颈,我会吃掉整个网站。

  public class Metadata { private HashMap validProgramCodes; private ReadWriteLock lock = new ReentrantReadWriteLock(); public HashMap getValidProgramCodes() { lock.readLock().lock(); try { return validProgramCodes; } finally { lock.readLock().unlock(); } } public void setValidProgramCodes(HashMap h) { lock.writeLock().lock(); try { validProgramCodes = h; } finally { lock.writeLock().unlock(); } } } 

我认为你的假设是正确的。 我唯一要做的就是设置validProgramCodes volatile。

 private volatile HashMap validProgramCodes; 

这样,当您更新validProgramCodes的“指针”时,您可以保证所有线程都访问相同的最新HasMap “指针”,因为它们不依赖于本地线程缓存并直接转到内存。

只要您不关心读取陈旧值,并且只要您可以保证在初始化时正确填充哈希映射,分配就会起作用。 您至少应该在Hashmap上使用Collections.unmodifiableMap创建hashMap,以保证您的读者不会更改/删除地图中的对象,并避免多个线程踩到彼此的脚趾并在其他线程销毁时使迭代器无效。

(上面的作者关于波动性是对的,应该看到的)

虽然这不是针对这个特定问题的最佳解决方案(erickson关于新的不可修改Map的想法),但我想花点时间提一下Java 5 中引入的java.util.concurrent.ConcurrentHashMap类,特别是HashMap的一个版本以并发为基础构建。 此构造不会阻止读取。

查看这篇关于并发基础的文章。 它应该能够令人满意地回答你的问题。

http://walivi.wordpress.com/2013/08/24/concurrency-in-java-a-beginners-introduction/

我认为这很危险。 线程导致各种微妙的问题,这是调试的巨大痛苦。 您可能希望查看FastHashMap ,它适用于这种只读线程的情况。

至少,我还声明validProgramCodesvolatile这样引用就不会被优化到寄存器或其他东西中。

如果我正确地读取了JLS (不保证那里!),对引用的访问总是primefaces的,句点。 见第17.7节“双primefaces和非primefaces的非primefaces化处理”

因此,如果对引用的访问始终是primefaces的,并且线程看到的返回的Hashmap的哪个实例Hashmap ,那么您应该没问题。 您永远不会看到对引用的部分写入。


编辑:在审查下面的评论中的讨论和其他答案后,这里是参考/引用

Doug Lea的书(Java中的并行编程,第2版),第94页,第2.2.7.2节“ 可见性” ,第3项:“

线程第一次访问对象的字段时,它会看到字段的初始值或自其他线程写入的值。“

在p。 94,Lea接着描述了与这种方法相关的风险:

内存模型保证,在最终发生上述操作的情况下,由一个线程对特定字段进行的特定更新最终将对另一个线程可见。 但最终可能是一个任意长的时间。

因此,当绝对肯定地,任何调用线程都必须可见时 ,需要volatile或其他同步障碍,特别是在长时间运行的线程或线程中访问循环中的值(如Lea所说)。

但是 ,如果存在短暂的线程 ,如问题所暗示的,新读者的新线程并且它不会影响应用程序读取陈旧数据, 不需要同步


在这种情况下,@ erickson的答案是最安全的,保证其他线程在发生时会看到对HashMap引用的更改。 我建议遵循这个建议只是为了避免对导致对此答案和下面的讨论进行“低票”的要求和实施的混淆。

我没有删除答案,希望它会有用。 我不是在寻找“Peer Pressure”徽章…… 😉