【问题标题】:ConcurrentHashMap and compound operationsConcurrentHashMap 和复合操作
【发布时间】:2025-11-21 02:10:01
【问题描述】:

Hashtable 和 Collections.synchronizedMap 是线程安全的,但仍然是复合操作,例如

if (!map_obj.containsKey(key)) {
   map_obj.put(key, value);
}

需要外部同步为:

synchronized(map_obj) {
    if (!map_obj.containsKey(key)) {
       map_obj.put(key, value);
    }
}

假设我们有 ConcurrentHashMap(CHM) 而不是 Hashtable 或 HashMap。 CHM为上述复合操作提供了另一种putIfAbsent()方法,从而无需外部同步。

但假设 CHM 没有提供putIfAbsent()。那么我们可以编写如下代码:

synchronized(concurrenthashmap_obj) {
    if (!concurrenthashmap_obj.containsKey(key)) {
       concurrenthashmap_obj.put(key, value);
    }
}

我的意思是我们可以在 CHM 对象上使用外部同步吗?它会工作吗?

对于上述复合操作,CHM 中有putIfAbsent() 方法,但是如果我们使用 CHM,我们如何实现其他复合操作的线程安全。我的意思是我们可以在 CHM 对象上使用外部同步吗?

【问题讨论】:

    标签: java collections thread-safety


    【解决方案1】:

    不,您不能使用外部同步来确保ConcurrentHashMap 上的复合操作的原子性。

    确切地说,您可以使用外部同步来确保复合操作的原子性,但前提是所有使用ConcurrentHashMap 的操作也都在同一个锁上同步(尽管在此使用ConcurrentHashMap 没有意义case - 你可以用普通的HashMap替换它。

    外部同步方法适用于HashtableCollections.synchronizedMap(),只是因为它们保证它们的原始操作也是synchronized 在这些对象上的。由于ConcurrentHashMap 不提供这样的保证,原始操作可能会干扰您的复合操作的执行,从而破坏它们的原子性。

    但是,ConcurrentHashMap 提供了许多可用于以乐观方式实现复合操作的方法:

    • putIfAbsent(key, value)
    • remove(key, value)
    • replace(key, value)
    • replace(key, oldValue, newValue)

    您可以使用这些操作来实现某些复合操作而无需显式同步,就像您对AtomicReference 等所做的一样。

    【讨论】:

    • 您的答案与其他答案相矛盾。
    • 但是对于像 putIfAbsent() 方法这样没有替代品的其他复合操作呢?
    • 已更新。您必须同步 ConcurrentHashMap 上的所有操作才能使其正常工作。
    • 感谢考虑另一个线程在地图上尝试基本冲突操作的情况。
    • @aLearner 简短版本:如果您使用 ConcurrentHashMap 进行同步,您做错了。该类的全部意义在于您不需要同步即可使用它;您可以使用一组不同的操作来避免这种成本。
    【解决方案2】:

    没有任何理由你不能。传统同步适用于所有事物,没有针对它们的特殊例外。 ConcurrentHashMaps 只是使用更优化的线程安全机制,如果您想做更复杂的事情,回退到传统同步实际上可能是您唯一的选择(即使用锁)。

    【讨论】:

      【解决方案3】:

      您始终可以使用synchronized 块。 java.util.concurrent 中的精美集合并没有禁止它,它们只是使其对于大多数常见用例来说是多余的。如果您正在执行复合操作(例如 - 您想要插入两个必须始终具有相同值的键),那么您不仅可以使用外部同步 - 您必须

      例如:

      String key1 = getKeyFromSomewhere();
      String key2 = getKeyFromSomewhereElse();
      String value = getValue();
      
      // We want to put two pairs in the map - [key1, value] and [key2, value]
      // and be sure that in any point in time both key1 and key2 have the same 
      // value
      synchronized(concurrenthashmap_obj) {
          concurrenthashmap_obj.put(key1, value);
      
          // without external syncronoziation, key1's value may have already been
          // overwritten from a different thread!
          concurrenthashmap_obj.put(key2, value);
      }
      

      【讨论】:

      • axtavt 的回答与您的回答相矛盾。我想听听您对 axtavt 给出的答案的看法。
      【解决方案4】:

      由于 ConcurrentHashMap 实现了 Map 接口,它确实支持每个基本 Map 的所有功能。所以是的:您可以像使用任何其他地图一样使用它,而忽略所有额外的功能。但是这样你就会有一个更慢的 HashMap。

      同步 Map 和并发 Map 之间的主要区别——顾名思义——并发性。想象一下,您有 100 个线程想要从 Map 中读取数据,如果您 synchronize 您阻塞了 99 个线程并且 1 个线程可以完成工作。如果您使用并发,100 个线程可以同时工作

      现在,如果您考虑使用线程的实际原因,您很快就会得出结论,您应该尽可能摆脱所有可能的 synchronized 块。

      【讨论】:

        【解决方案5】:

        这完全取决于“其他复合操作”和“工作”的含义。同步与 ConcurrentHashMap 的工作方式与它与任何其他对象的工作方式完全相同。

        因此,如果您希望将某些复杂的共享状态更改视为原子更改,那么对该共享状态的所有访问都必须在同一个锁上同步。这个锁可能是地图本身,也可能是另一个对象。

        【讨论】:

          【解决方案6】:

          关于java.util.concurrent.ConcurrentHashMap

          • “在依赖线程安全但不依赖同步细节的程序中与 Hashtable 完全互操作:它们不会抛出 ConcurrentModificationException。”

          • “允许更新操作之间的并发”

          关于java内存

          一般而言,从同步的角度来看,读取是安全的,但从内存的角度来看却不是。

          另见“http://www.ibm.com/developerworks/java/library/j-jtp03304/”。

          所以应该使用synchronizatonvolatile 来管理并发读取(相对于写入)。

          关于 putIfAbsent

          putIfAbsent 是你的friend

          如果指定的键尚未与值关联,则关联 它与给定的

          value. This is equivalent to
             if (!map.containsKey(key))
                 return map.put(key, value);
             else
                 return map.get(key);
          

          除了动作是!!!原子地执行!!!

          【讨论】: