HashMap 进行 put 操作时的流程是:先计算 hashCode 找到桶,然后遍历桶内的链表找到插入位置插入。即先查后写。但凡先查后写的,在并发环境下往往都有并发查询问题
举个例子,
比如有两个待写入的键值对 {“a”: 1, “b”: 2},刚好落到同一个桶里,假设这个桶是 0 号桶,这个桶里之前有一个元素 c。
单线程环境下
我们先查 a 准备写入的位置,查到是 0 号桶的 c 后面的位置,则我们写入 a,即 c.next = a
。然后查 b 准备写入的位置,这次查到是 0号桶的 a 后面的位置,写入 b,即 a.next = b
。此情景下 a, b 两个元素都能正常被写入。
多线程环境下
a 的查询可能和 b 是同步进行的。
比如,
a 在线程 t1 里,b 在线程 t2 里 线程 t1 的查询里,a 的写入位置是 c 后面,c.next=a
线程 t2 的查询里,b 的写入位置也是 c 后面(因为此刻 a 尚未插入),c.next=b
。
最终先插入 c 后面的会被后插入的覆盖,a 和 b 两个元素只有一个被实际成功写入,另一个丢了。
ConcurrentHashMap
所以说 hashMap 不能用于多线程环境,它本身就不是为多线程环境设计的。强行用于多线程环境会引发诸多问题。
那么 ConcurrentHashMap 是怎么解决这样的并发问题的呢?其实很简单,就是在写入时对例子中的 0号桶加锁,加锁期间别的线程需要等待锁释放后才能写入这个桶。
比如线程 t1 写入 a 时加了锁,那么线程 t2 必须等待 a 写完才能写入 b, 相当于把这个桶上的并行操作转为串行了。这时 b 的写入位置是 a 后面,a.next=b,a 和 b 都能正常被写入。(这是 jdk1.7 的做法,1.8 改为 CAS了,不再需要锁,不过不属于本问题范畴,这里不详谈)