提问者:小点点

在不使用putIfAbsend的情况下实现线程安全


我试图澄清关于类型安全和性能的HashMap与ConloctHashMap。我遇到了很多好文章,但仍然很难弄清楚这一切。

让我们使用ConCurrentHashMap以以下示例为例,我将尝试为尚未存在的键添加一个值并返回它,新的方法是:

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

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

    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)

这种方法的问题是整个映射被锁定,而在第一种方法中,putIfAbsend方法只在键的哈希所在的桶上同步,从而导致性能下降吗?第二种方法只使用HashMap可以正常工作吗?


共2个答案

匿名用户

这种方法的问题是整个地图被锁定了

这种方法有两个问题。

您在map引用上获得锁的事实没有任何影响,除了(尝试)获取此锁的任何其他代码。至关重要的是,ConCurrentHashmap本身不会获取此锁。

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

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

然后可能会发生您的map. get(key)调用返回null,但您的后续map.put调用最终会覆盖。换句话说,您的线程和运行putIfAbsend的假设线程都决定编写。

据推测,如果这在你的书中很好,那就太奇怪了。为什么要使用putIfAbsend检查map. get是否首先返回null

如果另一个线程这样做:

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

那么就没有问题了;要么您的get-check-if-null-then-set代码将设置并且putIfAbsend调用是noop,反之亦然,但他们不可能都“决定编写”。

这将我们引向;

使用map实现并发有两种不同的方式:内在和外在。两者都做是零点,它们不交互。

如果你有这样的结构,即从一个完全不支持多核的普通旧java. util.HashMap进行的所有访问(读取和写入)都要经过一些共享锁(hashmap实例本身或任何其他锁,只要与该特定映射实例交互的所有线程都使用相同的锁),那么这很好,因此没有理由或点来使用并发HashMap

并发HashMap的要点是在不使用外部锁定的情况下简化并发进程:让映射执行锁定。

您想要这样做的原因之一是ConloctHashMap impl在它能够完成的工作上速度明显更快;这些工作被明确地阐述了:这是并发HashMap拥有的方法。

代码片段的核心问题是它缺乏原子性。在并发模型中,check-then-act从根本上被破坏了(在你的例子中:检查:键“k”是否与无值或null相关联?,然后行动:设置键“k”到值“v”的映射)。这是坏的,因为如果你检查的东西在两者之间发生变化怎么办?如果你有两个线程都“检查和行动”,然后同时运行,然后它们都先检查,然后都先行动,然后坏的东西接踵而至:两个线程中的一个将作用于一个不等于你检查时状态的状态,这意味着你的检查坏了。

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

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

要么去掉并发映射,要么去掉同步。使用两者的代码可能是坏的,即使不是,它也容易出错,令人困惑,我可以向你保证有更好的编写方法(更好的是它更惯用,更容易阅读,面对未来的更改请求更灵活,更容易测试,并且不太可能有难以测试的错误)。

如果你可以根据CHM拥有的方法来陈述你需要100%执行的所有操作,那么就这样做,因为CHM非常优越。它甚至有任意操作的机制:例如,与基本hashmap不同,即使其他线程也在扰乱CHM,你也可以迭代CHM,而对于普通hashmap,你需要在整个操作期间保持锁,这意味着任何其他线程试图对该hashmap做任何事情,即使只是“询问其大小”,也需要等待。因此,对于大多数用例,CHM会带来数量级的更好性能。

匿名用户

在第一种方法中,putIfAbsend方法只在桶上同步

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

第二种方法仅使用HashMap是否可以正常工作?

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

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

假设我们不想使用putIfAbsend方法。

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

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

相关问题