深入解析ConcurrentHashMap中不能插入null键值对的原因及其复合操作的原子性问题

  1. 为什么 ConcurrentHashMap 的键(key)和值(value)不能为 null?
  2. ConcurrentHashMap 是否能够保证复合操作的原子性?

接下来,我将详细解答这两个问题,希望对你有所帮助。

为什么 ConcurrentHashMap 的键和值不能为 null?

ConcurrentHashMap 不允许键和值为 null,主要是为了消除二义性。null 作为一个特殊值,表示不存在的对象或引用。如果将 null 作为键存储在 ConcurrentHashMap 中,你将无法确定该键是否存在,或者根本没有该键。同样,如果将 null 作为值,你也无法判断这个值是存储在 ConcurrentHashMap 中的,还是因为找不到对应的键而返回的 null。

get 方法为例,返回 null 可以有两种情况:

  • 值并不存在于集合中;
  • 值本身即为 null。

这种情况造成了二义性的问题。

在多线程环境中,当一个线程对 ConcurrentHashMap 进行操作时,其他线程可能会同时修改该映射,因此无法依靠 containsKey(key) 来判断该键值对是否存在,从而无法解决二义性问题。

相比之下,HashMap 可以存储 null 的键和值,但对于 null 作为键的情况,最多只能存在一个,值可以有多个。如果传入 null 作为参数,在单线程环境中可以通过 contains(key) 方法进行判断,进而处理存在性问题,因此不存在二义性。

综上所述,单线程能够正确判断键值对的存在与否,而多线程则无法做到这一点。如果你确实需要在 ConcurrentHashMap 中使用 null,可以考虑使用一个特殊的静态空对象来替代 null:

public static final Object NULL = new Object();

最后,引用 ConcurrentHashMap 的作者 Doug Lea 对此问题的看法:

由于并发映射(如 ConcurrentHashMapConcurrentSkipListMap)中的二义性在非并发映射中可能略微可以容忍,因此在并发环境中不适用。主要原因是当 map.get(key) 返回 null 时,无法判断该键是显式映射到 null 还是该键根本不存在于映射中。在非并发映射中,可以通过 map.contains(key) 来检查,但在并发情况下,调用之间的映射可能已经发生变化。

ConcurrentHashMap 能保证复合操作的原子性吗?

ConcurrentHashMap 被设计为线程安全的,这意味着它能够保证多个线程同时读写时不会出现数据不一致的情况,也避免了 JDK 1.7 及之前版本 HashMap 的多线程操作问题。然而,这并不意味着所有复合操作都是原子性的,切勿混淆二者的概念!

复合操作是指由多个基本操作(如 putgetremovecontainsKey 等)构成的复杂操作。例如,首先检查某个键是否存在(containsKey(key)),然后根据结果进行插入或更新(put(key, value))。在执行过程中,可能会被其他线程打断,导致最终结果与预期不符。

假设有两个线程 A 和 B 同时对 ConcurrentHashMap 进行复合操作:

// 线程 A
if (!map.containsKey(key)) {
    map.put(key, value);
}
// 线程 B
if (!map.containsKey(key)) {
    map.put(key, anotherValue);
}

如果 A 和 B 的执行顺序如下:

  1. 线程 A 判断 map 中不存在 key;
  2. 线程 B 判断 map 中不存在 key;
  3. 线程 B 将 (key, anotherValue) 插入 map;
  4. 线程 A 将 (key, value) 插入 map。

最终结果将是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作非原子性导致的问题。

如何保证 ConcurrentHashMap 的复合操作原子性?

ConcurrentHashMap 提供了一些原子性的复合操作,比如 putIfAbsentcomputecomputeIfAbsentcomputeIfPresentmerge 等。这些方法可以接受一个函数作为参数,基于给定的键和值计算新的值,并将其更新到 map 中。

上述代码可以改写为:

// 线程 A
map.putIfAbsent(key, value);
// 线程 B
map.putIfAbsent(key, anotherValue);

或者:

// 线程 A
map.computeIfAbsent(key, k -> value);
// 线程 B
map.computeIfAbsent(key, k -> anotherValue);

虽然可以通过加锁来实现同步,但不推荐这样做,这与使用 ConcurrentHashMap 的初衷相违背。因此,使用这些原子性的复合操作方法更能保障操作的原子性。