【问题标题】:Java Concurrency: HashMap Vs ConcurrentHashMap when concurrent threads only remove elementsJava并发:并发线程仅删除元素时的HashMap与ConcurrentHashMap
【发布时间】:2019-09-25 03:23:27
【问题描述】:

我有一个创建 HashMap 的主线程,向其中添加多个可运行对象,然后启动每个可运行对象(将 HashMap 传递给每个对象)。可运行对象在即将完成处理之前从映射中移除其对象。

我想知道在这种情况下是否有任何理由使用 ConcurrentHashMap(而不是 HashMap) - 可运行对象在地图上执行的唯一操作是将自己从地图中移除。在这种情况下是否存在需要使用 ConcurrentHashMap 的并发性考虑?

主线程

private final Map<Integer, Consumer> runnableMap = new HashMap<>();

Runnable runnable;

for (int i = 1; i <= NUM_RUNNABLES; i++) {
    runnable = new Consumer(i, runnableMap);
    runnableMap.put(i, runnable);
    executionContext.execute(runnable);
}

消费者实现 Runnable

private final Integer consumerNumber;
private final Map<Integer, Consumer> runnableMap;

public Consumer(int consumerNumber, final Map<Integer, Consumer> runnableMap){
    this.consumerNumber = consumerNumber;
    this.runnableMap = runnableMap;
}

public void run() {
    :::
    // business logic
    :::
    // Below remove is the only operation this thread executes on the map
    runnableMap.remove(consumerNumber);
}

【问题讨论】:

  • 从地图中删除项目的代码不是HashMap 中的原子操作。并且还会出现可见性问题。您必须使用线程安全的ConcurrentHashMap
  • Kartik - 谢谢!但由于可运行文件不读取或更新地图中的任何元素,因此我不清楚在这种情况下可能出现的可见性问题。主线程是否能够在地图上看到已经完成执行的可运行元素?
  • 为什么?为什么不直接运行QueueRunnables?为什么不将该队列提供给ExecutorService?地图有什么用?
  • passing a copy of the HashMap to each 这不是副本,因为它看起来像是对同一个对象的引用。
  • michalk - 你是对的。它是相同的HashMap。我已经更正了。

标签: java concurrency concurrenthashmap


【解决方案1】:

如果您这样做的原因是为了跟踪线程完成,为什么不使用 CountdownLatch?不确定 HashMap 是否只能在删除时出现并发问题,我建议仅在您的代码不会因任何可能的问题而中断时才使用它,或者使用 ConcurrentHashMap。

【讨论】:

  • 谢谢马丁!可运行对象实际上是长时间运行的侦听器,仅在出现异常时才会终止。在任何给定时刻,我都希望能够从地图中确定活动可运行对象的数量,并从每个可运行对象中收集一些统计信息。主线程定期轮询地图以补偿任何“死”的可运行对象(通过添加新的)。
【解决方案2】:

javadoc of HashMap 说:

请注意,此实现不是 同步。

如果多个线程访问一个哈希映射 并发,并且至少有一个线程修改了映射 在结构上,它必须在外部同步。 (一种 结构修改是添加或删除一个的任何操作 或更多映射;仅更改与键关联的值 一个实例已经包含的不是结构的 修改。)这通常是通过同步来完成的 一些自然封装地图的对象。

如上所述,删除是一种结构变化,必须使用同步。

另外,在Hashmap的removeNode()方法中(由remove()方法调用),modCount变量自增,负责ConcurrentModificationException。因此,如果您在不同步的情况下删除元素,您可能会遇到此异常。

因此您必须使用ConcurrentHashMap

【讨论】:

  • re: 你必须使用 ConcurrentHashMap – OP 可以使用ConcurrentHashMapHashtable 获得正确的并发行为。并非绝对只有ConcurrentHashMap 可以工作。
  • @kaan 首先,OP 在 HashMap 与 ConcurrentHashMap 而不是 Hashtable 之间询问。其次,Hashtable 被认为是过时的。 ConcurrentHashMap 比 Hashtable 性能更好,因为它不需要锁定。
  • 同意,OP 询问了这两个问题,但对于一般意识,还有第三种选择。 Hashtable 自 JDK 1.0 以来就已经存在,但它仍然可用且未弃用。说 OP “必须使用” X 是不正确的。
  • @kaan 为什么不提一下 BlockingQueue 以及信号量和锁,以实现一般意识?在这种情况下,它们都可以使用。 “必须使用”是指“与 HashMap 相比”。仍然不是投反对票的好理由。
  • Kartik - 非常感谢!我发现您提供的上述 Javadoc 参考非常有帮助。我接受你的回答。
【解决方案3】:

您询问了HashMapConcurrentHashMap 之间的区别,但还有一个额外的数据结构需要考虑:Hashtable。每个都有差异和权衡。您需要评估哪个最适合您的预期用途。

  • HashMap 是不同步的,因此如果有多个线程可以读取或写入它,您的结果将无法预测。 HashMap 也允许 null 作为键或值。

  • Hashtable 已同步,不支持空键或空值。来自the Hashtable Javadoc

    Hashtable 已同步。如果不需要线程安全的实现,建议使用 HashMap 代替 Hashtable。如果需要线程安全的高并发实现,那么建议使用 ConcurrentHashMap 代替 Hashtable。

  • ConcurrentHashMap 是线程安全的,不允许将 null 用作键或值。

【讨论】:

  • 谢谢卡恩!鉴于可运行对象仅从映射中删除自己的实例并且不读取/更新任何元素,在这种情况下是否有任何线程安全问题需要担心?您能否帮助我了解我的主线程是否会立即看到删除,而不是在每个可运行的 HashMap 实现中发生?
  • 在您的情况下可能使用非并发结构,但我会提醒您仔细考虑。如果你最终推断你不需要并发保护但你错了,你可能会遇到非常混乱的行为。出于速度/性能考虑,有理由避免不必要的并发保护,但通常我会从可证明正确的代码开始,然后才考虑优化您的数据结构(也许)。
  • ConcurrentHashMap 不同步,但它是线程安全的。
猜你喜欢
  • 2021-12-12
  • 2020-10-11
  • 2018-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多