【问题标题】:ConcurrentHashMap thread-safety without using putIfAbsent不使用 putIfAbsent 的 ConcurrentHashMap 线程安全
【发布时间】:2020-12-13 00:03:25
【问题描述】:

我试图澄清 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 可以正常工作吗?

【问题讨论】:

    标签: java multithreading hashmap thread-safety concurrenthashmap


    【解决方案1】:

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

    这种方法有两个问题。

    这不是内在的

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

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

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

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

    大概,如果这在你的书中很好,那会很奇怪。为什么首先使用putIfAbsentcheck if map.get returns null

    其他线程是否这样做了:

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

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

    这导致我们;

    这是没有意义的

    有两种不同的方式来实现地图的并发性:内部和外部。两者都做是零点,它们不会相互作用。

    如果你有一个结构,all 访问(读取和写入)来自一个普通的、完全不支持多核的 java.util.HashMap 会通过一些共享锁(hashmap 实例本身或任何其他锁,只要与该特定地图实例交互的所有线程都使用相同的线程),那么工作正常,因此没有理由或指向使用ConcurrentHashMap

    ConcurrentHashMap 的重点在不使用外部锁定的情况下简化并发进程:让地图进行锁定。

    您想要这样做的原因之一是 ConcurrentHashMap impl 在其能够完成的工作上明显更快;这些工作都明确说明:这是 ConcurrentHashMap 拥有的方法。

    原子性

    您的代码 sn-p 的核心问题是它缺乏原子性。 Check-then-act 在并发模型中从根本上被破坏(在您的情况下:Check:键 'k' 是否与无值或 null 关联?然后 Act:设置键“k”到值“v”的映射)。这是坏的,因为如果你检查的东西在两者之间发生了变化怎么办?如果您有两个线程同时“检查并执行”然后同时运行怎么办?然后他们都先检查,然后都先行动,随后发生了破坏:两个线程之一将作用于与您检查时的状态不相等的状态,即表示您的支票已损坏。

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

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

    结论

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

    如果您可以按照 CHM 的方法说明您需要 100% 执行的所有操作,那么请执行此操作,因为 CHM 非常出色。它甚至具有任意操作的机制:例如,与基本的 hashmap 不同,即使其他线程也在搞乱它,您也可以遍历 CHM,而对于普通的 hashmap,您需要在 整个持续时间 操作,这意味着任何其他试图对该哈希图做任何事情的线程,即使只是“询问它的大小”,都需要等待。因此,对于大多数用例,CHM 会带来数量级的性能提升。

    【讨论】:

    • thx,对于您在线程中迭代 HashMap 的最后一点,而 get-check-if-null-then-set 代码正在由另一个(使用同步(map))执行,在这种情况下对于 HashMap,即使第一个线程也应该在 map 对象上获取锁,这意味着在 map 上执行任何操作的任何线程都应该在 map 上使用同步?对吗?在 CHM 的情况下,在这种特殊情况下使用除了 putIfAbsent 或 computeIfAbsent 之外的操作没有意义,任何其他操作都会导致我们进入 check-null-then-set 模式,从而强制使用 synchronize ?对吗?
    • 如果您没有 CHM,则:“这意味着在 map 上执行任何操作的任何线程都应该在 map 上使用同步?”是正确的。 “在 CHM 的情况下,使用除了 putIfAbsent 或 computeIfAbsent 之外的操作的特殊情况没有意义”是的 - 非原子事物不是原子的,你需要记住这一点。您可以阅读 CHM 的 iterator() 方法上的 javadocs,了解有关迭代行为的更多信息。
    【解决方案2】:

    在第一种方法中,putIfAbsent 方法仅在存储桶上同步

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

    仅使用 HashMap 就可以使用第二种方法吗?

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

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

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

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

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

    【讨论】:

    • 请注意,Collections.synchronizedMap 在这里大部分都没用。您不能以安全的方式执行if (!map.containsKey(k)) map.put(k, v);,即使map 是调用Collections.synchronizedMap 的结果(仅当您还添加同步块时),而例如CHM 的putIfAbsent(k, v) 确实完成了“添加映射 k->v 但前提是还没有 k 映射”的工作——不需要同步块。因此,CHM 不仅表现出色,synchronizedMap 甚至无法满足您的需求。
    • 谢谢,我明白了,即使使用 synchronizedMap 我也应该通过引入同步块来处理竞争条件,这意味着它不会在这里自行阻止竞争条件,所以 synchronizedMap 的重点是要进行同步操作,以便在其操作本身的执行过程中不会发生竞争条件(同步放置、同步获取、同步大小等),对吗?
    • 在 CHM 中,一个线程确实持有地图一部分的锁,对吗?
    • @saadoune 第一条评论正确。 --- 对于第二条评论,请参阅javadoc“但是,即使所有操作都是线程安全的,检索操作不需要锁定,并且不支持锁定以阻止所有访问的方式整个表。”
    猜你喜欢
    • 2012-08-20
    • 1970-01-01
    • 2014-03-04
    • 2019-09-22
    • 1970-01-01
    • 1970-01-01
    • 2011-04-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多