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

时间:2008-11-18 22:07:54

标签: java concurrency

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

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

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

到目前为止一切顺利。这是我们感兴趣的地方。

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

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

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

10 个答案:

答案 0 :(得分:28)

使用易失性

这是一个线程关心另一个线程在做什么的情况吗?然后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.等具体类型。这样可以提供灵活性将来如果具体类型需要改变(就像我在这里所做的那样)。

缓存

将“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:   // Do something with e.
 6: }

现在假设编写器线程在读取器的第2行和第3行之间的地图上调用“putAll”.Iterator底层的映射经历了并发修改,并抛出了运行时异常 - 一个非常间歇性的,看似无法解释的运行时异常在测试过程中从未产生过。

并发编程

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

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

其他资源

答案 1 :(得分:4)

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

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

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

答案 2 :(得分:3)

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

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

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

因此,在新的JMM下,引用volatile就足够了。它不太可能对系统性能产生重大影响。

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

答案 3 :(得分:3)

正如其他人已经指出的那样,这不安全,你不应该这样做。你需要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();
      }
    }
  }

答案 4 :(得分:2)

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

private volatile HashMap validProgramCodes;

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

答案 5 :(得分:1)

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

(上面的作者关于挥发性的是正确的,应该已经看过了)

答案 6 :(得分:1)

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

答案 7 :(得分:0)

查看此帖有关并发基础知识的信息。它应该能够令人满意地回答你的问题。

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

答案 8 :(得分:-1)

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

至少,我还要将validProgramCodes声明为volatile,以便引用不会优化到寄存器或其他内容。

答案 9 :(得分:-3)

如果我正确读取JLS(不保证!),对引用的访问总是原子的,句点。见Section 17.7 Non-atomic Treatment of double and long

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


编辑:在审阅下面的评论中的讨论和其他答案之后,这里有来自

的参考/引用

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

  

线程第一次访问字段   一个物体,它看到了   该字段的初始值或   自从其他人写的以来的价值   线程“。

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

  

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

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

然而 ,如果存在短命线程,正如问题所暗示的那样,为新读者提供了新线程, 不影响应用程序读取陈旧数据,同步


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

我没有删除答案,希望它有用。我寻找“同伴压力”徽章......; - )