这种方法的问题是整个地图都被锁定了
这种方法有两个问题。
这不是内在的
您已获得map 引用上的锁这一事实对任何其他代码(尝试)获得此锁的影响为零。至关重要的是,ConcurrentHashmap 本身不会获取此锁。
因此,如果在第二个 sn-p(同步)期间,其他线程执行此操作:
map.putIfAbsent(key, new Object());
然后,您的 map.get(key) 调用可能会返回 null,但您的后续 map.put 调用最终会被覆盖。换句话说,您的线程和运行 putIfAbsent 的假设线程都决定编写。
大概,如果这在你的书中很好,那会很奇怪。为什么首先使用putIfAbsent 和check 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 会带来数量级的性能提升。