我试图澄清关于类型安全和性能的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可以正常工作吗?
这种方法的问题是整个地图被锁定了
这种方法有两个问题。
您在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());