Java 中涉及 ConcurrentHashMap 的操作中的线程安全性

Thread Safety in operations involving ConcurrentHashMap in Java

提问人:HyperVol 提问时间:9/9/2023 最后编辑:Mark RotteveelHyperVol 更新时间:9/10/2023 访问量:60

问:

上下文:我正在创建一个新的,缓存它并返回它,但是,它的名称必须是唯一的。我们谈论的是多线程环境。我的问题在评论中。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 更糟糕 - 确保数据正确性,但对哈希映射的多次写入无用 - 因此会影响性能。

Java 并发 线程安全 同步

评论

1赞 user207421 9/10/2023
不要把你的问题放在评论中。您可以亲眼看到它们几乎是看不见的。把它们放在你问题的文本部分。

答:

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)putIfAbsentnew 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 考虑到存在一个充满多线程可能性的广阔世界,这真的取决于。即你在保护什么资源/为什么?您描述的操作是否需要采用相同的方法?为什么?他们需要与什么“同步”?