我试图澄清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)
这种方法的问题是整个Map被锁定,而在第一种方法中,putifabsent方法只在密钥散列所在的bucket上同步,从而导致性能降低吗?第二种方法只需要一个hashmap就可以了吗?
2条答案
按热度按时间eeq64g8w1#
这种方法的问题是整个Map都被锁定了吗
这种方法有两个问题。
这不是内在的
事实上你已经得到了
map
引用没有任何影响,除非涉及(尝试)获取此锁的任何其他代码。关键是,ConcurrentHashmap
它本身不获取此锁。因此,如果在第二个代码段(使用synchronized)期间,其他线程执行以下操作:
那么你的
map.get(key)
call返回null,但是您的后续操作map.put
呼叫结束时覆盖。换句话说,您的线程和运行putifabsent的假设线程都决定编写。想必,如果这在你的书中是好的,那就奇怪了。为什么使用
putIfAbsent
以及check if map.get returns null
首先呢?如果另一个线程这样做:
那就没问题了;要么get check if null then set code将被设置,putifabsent调用是noop,要么反之亦然,但它们不可能同时“决定写入”。
它引导我们去;
这是毫无意义的
有两种不同的方法来实现与Map的并发:内在的和外在的。两者都是零,它们不相互作用。
如果你有这样一种结构,所有的访问(读写)都是从一个普通的旧的完全不支持多核的
java.util.HashMap
通过某个共享锁(hashmap示例本身,或者任何其他锁,只要与该特定map示例交互的所有线程都使用同一个锁),那么就可以正常工作,因此没有理由或指向使用ConcurrentHashMap
相反。concurrenthashmap的要点是在不使用外部锁定的情况下简化并发进程:让Map执行锁定。
您希望这样做的原因之一是concurrenthashmap impl在它能够执行的任务上明显更快;这些工作是明确的:它是concurrenthashmap拥有的方法。
原子性
代码片段的中心问题是它缺乏原子性。check-then-act在并发模型中根本不存在(在您的示例中:check:key'k'与no value或null关联吗?then-act:set key'k'到value'v'的Map)。这是因为如果你检查的东西在两者之间发生了变化怎么办?如果有两个线程同时“检查并执行”,然后同时运行会怎么样;然后两个线程都先检查,然后都先执行操作,然后会出现损坏的情况:两个线程中的一个线程将执行的状态与您检查时的状态不同,这意味着您的检查已损坏。
正确的模式是先行动后检查:先行动,然后检查操作结果。当然,这需要重新定义并将代码片段中显式编写的代码集成到“行为”阶段的定义中。
换句话说,
putIfAbsent
不是一个方便的方法!是最基本的操作!这是唯一的方法(除了外在的锁定)来传达这样一个概念:“执行将‘v’与‘k’关联的动作,但前提是还没有关联。我将在下一步检查此操作的结果”。没有办法把它分解成if (!map.containsKey(key)) map.put(key, v);
因为check-then-act在并发建模中不起作用。结论
要么去掉concurrenthashmap,要么去掉synchronized。拥有同时使用这两种语言的代码可能会被破坏,即使不是这样,它也很容易出错,令人困惑,我可以向您保证有更好的方法来编写它(更好的是,它更惯用,更易于阅读,在面对未来的更改请求时更灵活,更易于测试,并且不太可能难以测试其中的bug)。
如果您可以按照chm拥有的方法100%地说明您需要执行的所有操作,那么就这样做吧,因为chm是非常优越的。它甚至有用于任意操作的机制:例如,与基本hashmap不同,即使其他线程也在处理chm,您也可以迭代chm,而对于普通hashmap,您需要在整个操作期间保持锁,这意味着任何其他线程都试图对该hashmap执行任何操作,即使只是“要求它的大小”,也需要等待。因此,对于大多数用例来说,chm会带来更好的性能。
raogr8fs2#
在第一种方法中
putIfAbsent
方法只在bucket上同步这是不正确的,
ConcurrentHashMap
不同步任何东西,它使用不同的机制,以确保线程安全。第二种方法只需要一个
HashMap
?是的,除了第二种方法有缺陷。如果使用同步制作
Map
线程安全,然后Map
应该使用同步。因此,最好打电话给Collections.synchronizedMap(map)
. 性能将比使用ConcurrentHashMap
.假设我们不想使用
putIfAbsent
方法。为什么?哦,因为如果钥匙已经在Map上了,那就浪费了分配,这就是为什么我们应该使用
computeIfAbsent()
相反