我最近偶然发现一篇文章,其中阐述了ConcurrentHashMap中执行原子操作的merge方法的重要性。下面是文章的链接:
https://www.nurkiewicz.com/2019/03/mapmerge-one-method-to-rule-them-all.html
它说,merge方法要么在给定的键下放置新值(如果不存在(,要么用给定的值更新现有的键(UPSERT(,并解释这个概念。然而,这正是put((方法的作用。
ConcurrentHashMap中的get((和put((在一起不是线程安全的。
请指导我理解merge((是如何将这个get((和put((放在一起并使ConcurrentHashMap线程上的操作安全的?
并发代码的一个关键概念是,两个原子操作一个接一个地执行,并不一定与单个原子操作执行相同的操作具有相同的效果。
让我们忽略映射尚未包含值的情况,因为这是一个不太有趣的值(仍然有一个竞赛条件需要处理,但另一个更有趣(。
因此,如果您已经有了一个给定键的值,那么merge
基本上就是put(key, mergeFunction(newValue, get(key))
。
如果你以这种方式实现它,那么你可能会遇到一个非常真实的丢失更新的问题:
- 您的线程执行
get(key)
并获取当前值(我们称之为v1( - 另一个线程将键的绑定更新为一个新值(我们称之为v2(
- 线程使用v1和要合并的参数计算新更新的值(我们称之为v3(
- 线程将键的绑定更新为新合并的值v3
请注意,对v2
的更新基本上被您的代码忽略(覆盖(。如果映射值是用来表示计数器的,那么这意味着您基本上完全忽略了一个更新,并且会得到错误的结果。
merge
提供了一种将更新应用于现有值的方法,而不必担心其他并发更新会将值从您的下面更改出来,从而丢失更新。
缺少的一点是merge
方法接受BiFunction
(重映射函数(,后者计算新值,而put则盲目插入/替换值。
重新映射函数接收旧值和新值,使用它们可以执行一些计算并返回新的更新值。