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

时间:2017-05-28 07:46:02

标签: java java.util.concurrent concurrenthashmap

来自ConcurrentHashMap#computeIfAbsent的Javadoc说

  

计算应简短,不得尝试   更新此地图的任何其他映射。

但是,从我看到的情况来看,在remove()中使用clear()mappingFunction方法可以正常工作。例如这个

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

mappingFunction中使用remove()方法有什么不好的后果?

3 个答案:

答案 0 :(得分:5)

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

{{1}}

此代码导致死锁。

答案 1 :(得分:4)

javadoc清楚地解释了原因:

  

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

你不必忘记ConcurrentHashMap旨在提供一种使用线程安全Map的方法,而不是像HashTable那样的旧线程安全Map类的情况。 /> 当对地图进行修改时,它仅锁定相关的映射而不是整个地图。

  

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

computeIfAbsent()是Java 8中添加的新方法 如果使用不当,也就是说,如果computeIfAbsent()的主体已经锁定了传递给方法的密钥的映射,则锁定另一个密钥,您输入的路径可能会导致{{1}的目的失效1}}最后你将锁定两个映射 想象一下,如果你在ConcurrentHashMap中锁定更多的映射并且该方法根本不短,那么问题就出现了。地图上的并发访问会变慢。

所以computeIfAbsent()的javadoc通过提醒computeIfAbsent()的原则来强调这个潜在问题:保持简单快速。

以下是说明问题的示例代码 假设我们有一个ConcurrentHashMap的实例。

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

  • 使用密钥ConcurrentHashMap<Integer, String>
  • 调用thread1的第一个主题:computeIfAbsent()
  • 使用密钥1
  • 调用thread2的第二个主题:computeIfAbsent()

2执行足够快的任务,但它并未遵循thread1 javadoc的建议:它会更新computeIfAbsent()中的密钥2,即另一个映射在方法的当前上下文中使用的那个(即密钥computeIfAbsent())。

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

对于这种特定情况,如果5000thread2之前启动,thread1map.put(2, someValue);的调用将被阻止,而thread1未返回thread2 }锁定键computeIfAbsent()的映射。

最后,我们得到一个2实例,可以在5秒内阻止键ConcurrentHashMap的映射,同时使用键2的映射调用computeIfAbsent()。登记/> 这是误导性的,没有效果,它违背了1意图和意图计算当前密钥值的ConcurrentHashMap描述:

  

如果指定的键尚未与值关联,则尝试   使用给定的映射函数计算其值并输入它   进入此地图,除非null

示例代码:

computeIfAbsent()

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

  

thread1:get()返回值2 = null

的值      

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

     

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

这是快速写的,因为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()返回值2 = null

的值

但是这个:

  

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

仅当返回thread2 ConcurrentHashMap时才输出

答案 2 :(得分:2)

这样的建议有点像不走在路中间的建议。你可以做到,你可能不会被汽车击中;如果你看到汽车即将到来,你也可能会离开。

但是,如果你刚刚在人行道(人行道)上停留,那么你会更安全。

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

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

所以,回答你的问题是什么可能是坏的结果:字面上的任何东西(好吧,任何正常完成或抛出RuntimeException);并且您不一定会在一段时间内或在不同的JVM上观察到相同的后果。

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