提问人:HyperVol 提问时间:9/9/2023 最后编辑:Mark RotteveelHyperVol 更新时间:9/10/2023 访问量:60
Java 中涉及 ConcurrentHashMap 的操作中的线程安全性
Thread Safety in operations involving ConcurrentHashMap in Java
问:
上下文:我正在创建一个新的,缓存它并返回它,但是,它的名称必须是唯一的。我们谈论的是多线程环境。我的问题在评论中。Item
class ItemOperations {
private ConcurrentMap<String, Item> store = new ConcurrentHashMap<>();
Item createItem(String name) throws Exception {
// I agree this operation is thread safe because of ConcurrentHashMap
if (store.containsKey(name)) {
throw new Exception("Already Exists");
}
// Item creation is Expensive operation
Item newItem = new Item();
// *Ques 1* : For above - multiple threads will create multiple different objects right ?
// putIfAbsent is again thread safe due to concurrentMap..
// Losing thread will not update the value.. (correct me if i'm wrong)
store.putIfAbsent("newItemName", newItem);
// *Ques 2* : Separate threads will return different objects and
// the second thread will return a new object which is
//NOT present in the store but was created on the fly , hence returning incorrect data
return newItem;
}
}
如何解决这个问题?
我的想法:整个createItem()应该包装在一个synchronized(this)块中吗?以便 Item 对象创建是线程安全的?如果是,我们将失去 ConcurrentHashmap 的好处,并且会进一步降低性能。
使用 put 而不是 putIfAbsent 更糟糕 - 确保数据正确性,但对哈希映射的多次写入无用 - 因此会影响性能。
答:
2赞
DuncG
9/9/2023
#1
您忽略了允许您返回一致值的返回值。putIfAbsent
但是,您可以通过一次调用来替换大多数方法
return map.computeIfAbsent(name, key -> new Item());
评论
0赞
HyperVol
9/10/2023
这是正确的,但是putIfAbsent返回旧值,所以我怀疑它在这里是否有用
2赞
DuncG
9/10/2023
@Hypervol 你的逻辑是有缺陷的,因为两个线程可以作为假线程过去,并且两个线程都计算 .一个 putIfAbsent 调用将返回 null,另一个调用将返回另一个线程的 。由于不进行检查,“失败”调用方会返回不同版本的 newItem。同名的回报永远不会不一致。store.containsKey(name)
putIfAbsent
new Item()
computeIfAbsent
2赞
user207421
9/10/2023
@HyperVol 没错,它返回旧值而不是放置新值。这怎么没有用?
2赞
Alexander Katsenelenbogen
9/9/2023
#2
如上所述,您的方法不是线程安全的。为什么?
此方法的两个线程调用可能会到达此行并看到缺少的键:
if (store.containsKey(name)) {
throw new Exception("Already Exists");
}
如果两个调用都看到缺少名称键,则您的项目现在将创建两次。那么这里有什么教训呢?单独使用 ConcurrentHashMap 是不够的,至少不能以你使用它的方式。
您要做的是利用 ConcurrentHashMap 的原子方法。DuncG 发布的答案提供了一种方法:
return map.computeIfAbsent(name, () -> new Item());
现在,ConcurrentHashMap 会为您处理线程安全,您无需使用 synchronized 关键字。实际上,它将检查地图,如果缺少对象,则创建对象,然后以原子方式返回它,而无需担心竞争条件。
综上所述,如果您的用例可以容忍两个或多个 Item() 的创建以及两个或更多版本(偶然)浮动,那么这可能没什么大不了的。也就是说,我仍然建议使用 computeIfAbsent 方法,因为它可以保证一致的行为。
评论
0赞
HyperVol
9/10/2023
对于这种情况来说听起来不错。但是,如果同一方法中还有其他一些操作,需要原子/线程保护(操作不在存储上,而是其他一些变量/资源),在这种情况下,您有什么建议?
1赞
Alexander Katsenelenbogen
9/11/2023
@HyperVol 考虑到存在一个充满多线程可能性的广阔世界,这真的取决于。即你在保护什么资源/为什么?您描述的操作是否需要采用相同的方法?为什么?他们需要与什么“同步”?
评论