【问题标题】:HashMap or ConcurrentHashMap for single writer / single reader scenario?单写/单读场景的 HashMap 或 ConcurrentHashMap?
【发布时间】:2017-03-11 21:38:32
【问题描述】:

这里是详细信息。

  1. 一个线程写入地图,另一个线程读取地图。
  2. 密钥一旦插入就永远不会更新。
  3. 如果消费者线程在映射中没有找到该值,它可以自己计算该值。它不会找到 1% 的值,因为它们不会由生产者线程计算。
  4. 当消费者要求一个价值时,它应该尽快得到它。

HashMap 在这种情况下是否可以不进行同步工作?或者我将不得不使用ConcurrentHashMap

【问题讨论】:

  • 不,是的。 HashMap 不是线程安全的。因此同时写入和读取它是不安全的。
  • 当然详细信息在描述中。否决愚蠢的标题。
  • 因为你的“读者”也可以计算一个值,你可以让它用computeIfAbsent把这个值放到地图中

标签: java multithreading hashmap concurrenthashmap


【解决方案1】:

如果您计划同时从多个线程读取和写入映射,则需要ConcurrentHashMapHashMap 不会这样做,因为尝试同时执行 getput 可能会导致不正确的行为。

如果写线程在读线程开始之前完成,你可以使用普通的HashMap

【讨论】:

  • ConcurrentHashMap 有替代品,但不一定更好。您选择最适合用例的一种。
【解决方案2】:

简短的回答是您需要ConcurrentHashMap

稍微长一点的解释是有两个原因。首先,一个简单的HashMap 冒着读取器和写入器线程同时处理HashMap 的相同内部数据的风险。对于HashMap,要保持其正确性,它显然必须以一种对任何客户端都必须作为单个操作发生的方式更新多个内部属性。允许读者在作者忙于修改这些属性时查询HashMap 可能会导致意外行为。

第二点是,如果不使用 Java 的并发支持机制,一个线程所做的更改的可见性无法以可预测的方式提供给其他线程。因此,即使作者在读者查询 HashMap 之前完成,也不能保证读者会在作者离开时看到数据 - ConcurrentHashMap 可以解决这个问题。

第一点更可能发生在单处理器机器上,其中写入线程可能会完成插入新值的部分工作,然后在读取线程从部分更新的映射中读取时产生。第二点是多核机器上的一个问题,其中每个内核都有自己的共享内存版本,只有在您使用并发机制时才能与其他内核同步。

【讨论】:

  • “允许读者在作者忙于修改这些属性时查询 HashMap 可能会导致意外行为。” - 你有一个合理的场景吗?从某种意义上说,读者不会特意检测竞争条件是合理的。
  • 嗯,最明显的操作是当 HashMap 需要增加容量时。如果读取器通过获取对内部表的引用开始查询,然后让位于调整大小并重建表的写入器,则读取器将继续引用已被 HashMap 完全丢弃的表。
  • 关于调整大小的要点。虽然我不担心读取旧表(尽管这可能会导致读者在某些实现中错过旧条目),但如果作者决定在完成写入之前更改表引用,读者可能会错过数据。很高兴在我编写 1W1R 无锁哈希图时意识到这一点。
  • 如果一个键值对一旦插入就永远不会更新,[就像我的情况一样],调整大小和旧引用应该不是问题。我说的对吗?
  • 否 - 考虑一个键在调整大小操作之前可能属于一个哈希桶,之后可能属于另一个哈希桶。此外,您需要考虑(可能不太可能)具有大量冲突的哈希桶可能被重新配置为树结构的情况(在 Java 8 中)。如果您不使用 Java 的并发机制,这就是您需要考虑的事情。
【解决方案3】:

我正在根据您的问题进行回复。

您应该使用ConcurrentHashMap,因为它是线程安全的,而HashMap 不是线程安全的。

【讨论】:

    【解决方案4】:

    假设读取器和写入器是不同的线程,那么您确实需要一个线程安全的数据结构。这排除了使用裸(即非同步)HashMap

    我能想到三种选择。

    • 带有外部同步的HashMap。 (这需要谨慎实施。)

    • 使用Collections.synchronizedMap(...) 包装HashMap(或另一个Map 类)。如果读者和作者只是在做gets和puts,这将起作用。但是,如果需要对地图进行迭代,就会出现问题。

    • 使用ConcurrentHashMap。唯一需要担心的是,ConcurrentHashMaps 对高争用的支持在低争用用例中可能会有点昂贵。

    当然,ConcurrentHashMap 是最简单的选择。

    【讨论】:

    • 值得注意的是,ConcurrentHashMap 的性能可能要好得多。使用Collections.synchronizedMap() 调用意味着只有单个线程可以一次访问映射,而ConcurrentHashMap 允许并发访问,特别是当不同线程处理不同键时。
    【解决方案5】:

    这是一个很好的问题。源代码实际上看起来你可以摆脱它。看起来废弃节点中不会有任何循环导致您的线程卡住或发生任何事情。您最糟糕的情况似乎是您在调整大小或树化操作期间看不到实际存在的值。但是无论如何你都必须在读取线程中计算 1% 的值,所以这可能不会打扰你。

    但我们需要在这里多考虑一下。内存重新排序可以为您提供对未初始化对象或部分初始化对象的引用。

    我相信部分初始化的Node 甚至会给你带来误报。如果Node 来自内存中的某个位置,该位置以前也持有Node,那么如果您从并发 put 中看到其中一个,并且其中一个来自于在垃圾收集之前。

    这极不可能,内存重新排序可能只会给你更多的假阴性,但我会远离它,除非你有一种非常快速的方法来验证值。

    【讨论】:

      猜你喜欢
      • 2012-08-12
      • 1970-01-01
      • 2012-05-08
      • 1970-01-01
      • 2010-11-25
      • 2010-11-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多