这可能是一个重复的问题,但我在一本关于并发的书中发现了这部分代码。据说这是线程安全的:
ConcurrentHashMap<String, Integer> counts = new ...;
private void countThing(String thing) {
while (true) {
Integer currentCount = counts.get(thing);
if (currentCount == null) {
if (counts.putIfAbsent(thing, 1) == null)
break;
} else if (counts.replace(thing, currentCount, currentCount + 1)) {
break;
}
}
}
从我(并发初学者(的角度来看,线程t1和线程t2都可以读取currentCount = 1
。然后两个线程都可以将映射的值更改为2。有人能解释一下代码是否正常吗?
诀窍在于replace(K key, V oldValue, V newValue)
为您提供原子性。来自文档(强调我的(:
如果当前映射到给定值,则仅替换键的条目。。。该动作是以原子方式执行的。
关键词是"原子性的"。在replace
中,"检查旧值是否是我们所期望的,只有当它是时,才替换它"作为一个单独的工作块发生,没有其他线程能够与它交织。这取决于实现是否进行所需的任何同步,以确保它提供这种原子性。
因此,不可能两个线程都从replace
函数中看到currentAction == 1
。其中一个会将其视为1,因此它对replace
的调用将返回true。另一个会将其视为2(因为第一次调用(,因此返回false—并且循环返回以重试,这一次使用currentAction == 2
的新值。
当然,可能是第三个线程同时将currentAction更新为3,在这种情况下,第二个线程将继续尝试,直到幸运地没有人跳到它前面
有人能解释一下代码是否正常吗?
除了yshavit的答案之外,您还可以通过使用Java8中添加的compute
来避免编写自己的循环。
ConcurrentMap<String, Integer> counts = new ...;
private void countThing(String thing) {
counts.compute(thing, (k, prev) -> prev == null ? 1 : 1 + prev);
}
使用put也可以替换值。
if (currentCount == null) {
counts.put(thing, 2);
}