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

Vit*_*nko 8 java concurrenthashmap java.util.concurrent

Javadoc ConcurrentHashMap#computeIfAbsent

计算应该简短,并且不得尝试更新此映射的任何其他映射.

但是,从我看到的,使用remove()clear()内部方法mappingFunction工作正常.例如这个

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

在内部使用remove()方法有什么不好的后果mappingFunction

Era*_*ran 5

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

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

此代码导致死锁.


dav*_*xxx 4

javadoc清楚地解释了原因:

当计算正在进行时,其他线程对此映射的某些尝试更新操作可能会被阻止,因此计算应该简短且简单,并且不得尝试更新此映射的任何其他映射。

您不要忘记,它的ConcurrentHashMap设计目的是提供一种使用线程安全 Map 的方法,而无需像旧线程安全 Map 类那样锁定HashTable
当映射发生修改时,它仅锁定相关映射而不是整个映射。

ConcurrentHashMap是一个哈希表,支持检索的完全并发性和更新的高预期并发性。

computeIfAbsent()是Java 8中添加的一个新方法。
如果使用不当,也就是说,如果在该方法的主体中computeIfAbsent()已经锁定了传递给该方法的键的映射,那么您锁定了另一个键,那么您将进入一条可能无法达到目的的路径最后ConcurrentHashMap你将锁定两个映射。想象一下,如果您在内部锁定更多映射并且该方法一点也不短,会
出现什么问题。computeIfAbsent()地图上的并发访问会变慢。

因此,javadoccomputeIfAbsent()通过提醒以下原则来强调这个潜在问题ConcurrentHashMap:保持简单和快速。


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

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

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

thread1执行足够快的任务,但它不遵循 javadoc 的建议:它更新中的computeIfAbsent()key ,这是方法当前上下文中使用的另一个映射(即 key )。 执行足够长的任务。它按照 javadoc 的建议使用密钥进行调用:它不会更新其实现中的任何其他映射。 为了模拟长任务,我们可以使用带有参数的方法。 对于这种特定情况,如果在before 之前启动,则in的调用将被阻止,而不会返回锁定键的映射。2computeIfAbsent()1

thread2computeIfAbsent()2
Thread.sleep()5000

thread2thread1map.put(2, someValue);thread1thread2computeIfAbsent()2

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

如果指定的键尚未与值关联,则尝试使用给定的映射函数计算其值并将其输入到此映射中,除非为 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();
  }
}
Run Code Online (Sandbox Code Playgroud)

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

thread1 : get() 返回键 2 = null 的值

thread2 :computeIfAbsent() 返回键 2 的值 = valueSetByThread2

thread1 : put() 返回后,键 2 的先前值 = valueSetByThread2

写入速度很快,因为读取ConcurrentHashMap不会阻塞:

thread1 : get() 返回键 2 = null 的值

但是这个 :

thread1 : put() 返回后,键 2 的先前值 = valueSetByThread2

仅当 thread2 返回时输出computeIfAbsent()