不使用 putIfAbsent 的 ConcurrentHashMap 线程安全

ConcurrentHashMap thread-safety without using putIfAbsent

我正在尝试澄清 HashMap 与 ConcurrentHashMap 的类型安全性和性能。看到了很多好文章,但还是搞不清楚。

让我们来看下面这个使用 ConcurrentHashMap 的例子,我将尝试为一个不存在的键添加一个值并返回它,新的方法是:

    private final Map<K,V> map = new ConcurrentHashMap<>();
    return map.putIfAbsent(k, new Object());

假设我们不想使用 putIfAbsent 方法,上面的代码应该如下所示:

    private final Map<K,V> map = new ConcurrentHashMap<>();
    synchronized (map) {
        V value = map.get(key); //Edit adding the value fetch inside synchronized block 
        if (!nonNull(value)) {
            map.put(key, new Object());
        }
    }
    return map.get(key)

这种方法的问题是整个映射都被锁定,而在第一种方法中,putIfAbsent 方法仅在密钥散列所在的存储桶上同步,从而导致性能下降?第二种方法仅使用 HashMap 是否可以正常工作?

Is the problem with this approach the fact that the whole map is locked

这种方法有两个问题。

它不是内在的

您已获得 map 引用上的锁这一事实对任何其他(尝试)获得此锁的代码都没有任何影响。至关重要的是,ConcurrentHashmap 本身 不会 获取此锁。

因此,如果在第二个片段(使用同步)期间,某个其他线程执行此操作:

map.putIfAbsent(key, new Object());

然后您的 map.get(key) 调用 returns 可能会出现空值,但是您的后续 map.put 调用最终会被覆盖。换句话说,您的线程和假设的线程 运行ning putIfAbsent 都决定写入。

据推测,如果这在您的书中很好,那就很奇怪了。为什么首先使用 putIfAbsentcheck if map.get returns null

如果另一个线程这样做了:

synchronized (map) {
  map.putIfAbsent(key, new Object());
}

那就没问题了;要么你的 get-check-if-null-then-set 代码将被设置并且 putIfAbsent 调用是一个 noop,反之亦然,但它们不可能都 'decide to write'.

这导致我们;

这毫无意义

有两种不同的方法可以通过映射实现并发:内部和外部。两者都做零分,互不影响

如果您的结构使所有访问(读取和写入)一个普通的旧的完全非多核能力java.util.HashMap通过一些共享锁( hashmap 实例本身,或任何其他锁,只要与该特定 map 实例交互的所有线程都使用相同的锁),那么 工作正常 因此没有理由或指向使用ConcurrentHashMap 相反。

ConcurrentHashMap的要点是精简并发进程而不使用外部锁:让map做锁。

您想要这样做的原因之一是 ConcurrentHashMap 实现显着更快地完成它能够完成的工作;这些工作明确说明:这是 ConcurrentHashMap 具有的方法。

原子性

您的代码片段的核心问题是它缺乏原子性。 Check-then-act 在并发模型中从根本上被打破了(在你的例子中:Check: Is key 'k' associated with no value or null?, then Act :设置键'k'到值'v'的映射。这被打破了,因为如果你检查的东西在两者之间发生变化怎么办?如果您有两个同时 'check-and-act' 和 运行 的线程怎么办?然后他们都先检查,然后都先行动,然后发生故障:两个线程中的一个将作用于一个不等于你时的状态的状态checked,这意味着你的支票坏了。

正确的模式是act-then-check:先行动,再检查操作结果。当然,这需要重新定义您在代码段中明确编写的代码并将其集成到 'act' 阶段的定义中。

换句话说,putIfAbsent不是一个方便的方法!是一个基本操作!这是传达以下概念的唯一方法(除了外部锁定):“执行将 'v' 与 'k' 关联的操作,但前提是还没有关联。我将检查结果接下来这个操作”。无法将其分解为 if (!map.containsKey(key)) map.put(key, v);,因为先检查后操作在并发建模中不起作用。

结论

要么去掉concurrenthashmap,要么去掉synchronized。使用两者的代码可能已损坏,即使没有损坏,也容易出错,令人困惑,我可以向您保证有更好的编写方法(更好的是它更惯用,更易于阅读,更灵活面对未来的变更请求,更容易测试,并且不太可能有难以测试的错误。

如果您可以按照 CHM 具有的方法 100% 地说明您需要执行的所有操作,那么就这样做,因为 CHM 非常优越。它甚至具有任意操作的机制:例如,与基本的 hashmap 不同,您可以遍历 CHM,即使其他线程也在搞乱它,而对于普通的 hashmap,您需要为整个 持有锁duration 操作,这意味着任何其他线程试图对该 hashmap 执行任何操作,即使只是 'ask for its size',也需要等待。因此,对于大多数用例,CHM 的性能提高了几个数量级。

in first approach the putIfAbsent method only synchronizes on the bucket

这是不正确的,ConcurrentHashMap 不同步任何东西,它使用不同的机制来确保线程安全。

Would the second approach work fine with just a HashMap ?

是的,除了第二种方法有缺陷。如果使用同步使 Map 线程安全,那么 Map 的所有访问都应该使用同步。因此,最好调用 Collections.synchronizedMap(map)。性能会比使用 ConcurrentHashMap.

private final Map<Integer, Object> map = Collections.synchronizedMap(new HashMap<>());

let's assume we don't want to use the putIfAbsent method.

为什么?哦,因为如果键已经在映射中,它会浪费分配,这就是为什么我们应该使用 computeIfAbsent() 而不是

map.computeIfAbsent(key, k -> new Object());