在 ConcurrentHashMap#computeIfAbsent 中更新其他键的后果

Consequences of updating other key(s) in ConcurrentHashMap#computeIfAbsent

来自 ConcurrentHashMap#computeIfAbsent 的 Javadoc 说

The computation should be short and simple, and must not attempt to update any other mappings of this map.

但是,据我所知,在 mappingFunction 中使用 remove()clear() 方法效果很好。例如这个

Key element = elements.computeIfAbsent(key, e -> {
    if (usages.size() == maxSize) {
        elements.remove(oldest);
    }
    return loader.load(key);
});

mappingFunction 中使用 remove() 方法会产生什么不良后果?

这是一个不良后果的例子:

ConcurrentHashMap<Integer,String> cmap = new ConcurrentHashMap<> ();
cmap.computeIfAbsent (1, e-> {cmap.remove (1); return "x";});

此代码导致死锁。

这样的忠告有点像不要走在马路中间的忠告。你能做到,你可能不会被车撞到;如果你看到车来了,你也可以让开。

但是,如果您一开始就留在人行道上,您会更安全。

如果 API 文件告诉你不要做某事,当然没有什么能阻止你去做。你可能会尝试这样做,并发现没有不良后果,至少在你测试的有限情况下是这样。您甚至可以深入了解建议存在的确切原因;您可以仔细检查源代码并证明它在您的用例中是安全的。

但是,API 的实施者可以在 API 文档描述的合同约束范围内自由更改实施。他们可能会做出更改,使您的代码明天停止工作,因为他们没有义务保留他们明确警告不要使用的行为。

因此,回答您的问题,即不良后果可能是什么:几乎任何事情(好吧,任何正常完成或抛出 RuntimeException 的事情);随着时间的推移或在不同的 JVM 上,您不一定会观察到相同的结果。

留在人行道上:不要做文档告诉你不要做的事情。

javadoc 清楚地解释了原因:

Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this map.

您不要忘记 ConcurrentHashMap 旨在提供一种使用线程安全 Map 的方法,而无需像旧线程安全 Map 类 那样锁定,因为 HashTable.
当地图发生修改时,它只锁定相关的地图而不是整个地图。

ConcurrentHashMap is a hash table supporting full concurrency of retrievals and high expected concurrency for updates.

computeIfAbsent()是Java8中新增的方法
如果使用不当,也就是说,如果在 computeIfAbsent() 的主体中已经锁定了传递给该方法的键的映射,您锁定了另一个键,您进入的路径可能会破坏 [= 的目的11=] 最后你将锁定两个映射。
想象一下,如果您在 computeIfAbsent() 中锁定更多映射并且该方法一点也不短,那么问题来了。地图上的并发访问会变慢。

因此 computeIfAbsent() 的 javadoc 通过提醒 ConcurrentHashMap 的原则强调了这个潜在问题:保持简单和快速。


这是说明问题的示例代码。
假设我们有一个 ConcurrentHashMap<Integer, String>.

的实例

我们将启动两个使用它的线程:

  • 第一个线程:thread1 使用密钥 1
  • 调用 computeIfAbsent()
  • 第二个线程:thread2 使用键 2
  • 调用 computeIfAbsent()

thread1 执行了一个足够快的任务,但它没有遵循 computeIfAbsent() javadoc 的建议:它更新 computeIfAbsent() 中的键 2,这是另一个映射其中一个在当前上下文中使用的方法(即键1)。

thread2 执行足够长的任务。它通过遵循 javadoc 的建议使用密钥 2 调用 computeIfAbsent():它不会在它的实现中更新任何其他映射。
为了模拟长任务,我们可以使用参数为5000Thread.sleep()方法。

对于这种特定情况,如果thread2thread1之前开始,那么thread1map.put(2, someValue);的调用将被阻塞,而[=14=的thread2不会返回] 锁定键的映射 2.

最后,我们得到一个 ConcurrentHashMap 实例,它在 5 秒内阻止键 2 的映射,而 computeIfAbsent() 通过键 1 的映射被调用.
它具有误导性,无效,并且违背了 ConcurrentHashMap 意图和 computeIfAbsent() 意图计算当前键值的描述:

if the specified key is not already associated with a value, attempts to compute its value using the given mapping function and enters it into this map unless null

示例代码:

import java.util.concurrent.ConcurrentHashMap;

public class BlockingCallOfComputeIfAbsentWithConcurrentHashMap {

  public static void main(String[] args) throws InterruptedException {
    ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();

    Thread thread1 = new Thread() {
        @Override
        public void run() {
            map.computeIfAbsent(1, e -> {
                String valueForKey2 = map.get(2);
                System.out.println("thread1 : get() returns with value for key 2 = " + valueForKey2);
                String oldValueForKey2 = map.put(2, "newValue");
                System.out.println("thread1 : after put() returns, previous value for key 2 = " + oldValueForKey2);
                return map.get(2);
            });
        }
    };

    Thread thread2 = new Thread() {
        @Override
        public void run() {
          map.computeIfAbsent(2, e -> {
            try {
              Thread.sleep(5000);
            } catch (Exception e1) {
              e1.printStackTrace();
            }
            String value = "valueSetByThread2";
            System.out.println("thread2 : computeIfAbsent() returns with value for key 2 = " + value);
            return value;
          });
        }
    };

    thread2.start();
    Thread.sleep(1000);
    thread1.start();
  }
}

作为输出,我们总是得到:

thread1 : get() returns with value for key 2 = null

thread2 : computeIfAbsent() returns with value for key 2 = valueSetByThread2

thread1 : after put() returns, previous value for key 2 = valueSetByThread2

这写得很快,因为 ConcurrentHashMap 上的读取没有阻塞:

thread1 : get() returns with value for key 2 = null

但是这个:

thread1 : after put() returns, previous value for key 2 = valueSetByThread2

只有在computeIfAbsent().

返回thread2时才输出