假设我们有 10 个线程使用不同的键值调用以下代码。提供给computeIfAbsent方法的“Function”参数是否并行运行,或者computeIfAbsent将“锁定”整个表?
Map<String, String> map = new ConcurrentHashMap<>();
map.computeIfAbsent(key, K -> { // long time operation });
有两种方法可以解释这个问题。
第一个是,理论上,ConcurrentHashMap.computeIfAbsent
方法的
规范是否保证仅在正在计算的特定键上同步?这个问题的答案直接来自该方法的文档:
在计算过程中,其他线程对此映射的某些尝试更新操作可能会被阻止,因此计算应该简短且简单,并且不得尝试更新此映射的任何其他映射。
这对于它是在整个映射上同步还是仅在单个键上同步是不明确的,但它没有明确承诺在计算 value-if-absent 时可以在其他线程中进行其他键的更新。它表示“某些尝试的更新操作”可能会被阻止,但不会限制哪些操作或有多少操作被阻止。所以严格的答案是,不,允许一致的实现在整个地图对象上同步,并阻止所有其他更新。
问题的第二种解释是,实际上,该方法的是否仅在单个密钥上同步?这个问题的答案将取决于哪个实现,但将来自该实现的源代码。 来自
OpenJDK 8 实现:
Node<K,V> f;
// ...
if(/* ... */) {
// ...
} /* ... */ else if(/* ... */) {
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
// ...
}
// ...
} /* ... */ else {
// ...
synchronized (f) {
// ...
}
// ...
}
所以答案(至少如果您使用此实现)是是
,实际上,该方法在代表单个键/值对而不是整个映射的对象(f
或
r
)上同步,因此在计算函数时不应阻止对其他键的更新。ConcurrentHashMap.computeIfAbsent
会阻止其他一些键。 下面的代码将在为
i==1
添加值后阻塞。在 OpenJDK 17 上。public static void main(String[] args) throws Exception {
var c = new ConcurrentHashMap<Integer, Boolean>(1);
var cc = new CountDownLatch(1);
new Thread(() -> {
c.computeIfAbsent(0, k -> {
cc.countDown();
System.out.println("start");
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
});
}).start();
cc.await(1, TimeUnit.HOURS);
System.out.println("adding");
for (int i = 1; i < 10_000; i++) {
System.out.println("i: " + i);
c.putIfAbsent(i, true);
}
System.out.println("done");
}