ConcurrentHashMap是一个支持高并发更新与查询的哈希表(基于HashMap)。Hashmap在多线程并发的情况下,是线程不安全的,容易出现死循环、死锁等问题,JDK8后不会出现死锁问题,但依然存在多线程的系列问题,如:数据丢失等。所以就有了ConcurrentHashMap 来解决了。
ConcurrentHashMap在JDK1.7是通过segment来对map进行分段(分为多个Segment),每个segment持有一把锁,在多线程并发访问的时候,每个线程对应一个segment,各segment之间操作互不影响。ConcurrentHashMap初始化segment的个数为16,也就等于支持16个线程同时操作,如果有大于16或更多线程同时访问,且都是put插入操作就需要等待释放锁后,才能继续操作,写与读是可以并发执行的。具体可参考下图说明:
ConcurrentHashMap集合中有2的N次方Segment对象,共同保存在一个名为 segments的数组当中。因此整个ConcurrentHashMap的结构如下:
不同 Segment 的并发写入【可以并发执行】
同一Segment的一写一读【可以并发执行】
同一Segment的并发写入【需要上锁】
Concurrent的读写过程
Get方法:
- 为输入的Key做Hash运算,得到hash值(为了实现Segment均匀分布,进行了两次Hash)。
- 通过 hash 值,定位到对应的 Segment 对象。
- 再次通过 hash 值,定位到 Segment 当中数组的具体位置。
Put方法:
- 为输入的Key做Hash运算,得到hash值。
- 通过hash值,定位到对应的Segment对象
- 获取可重入锁
- 再次通过hash值,定位到Segment当中数组的具体位置。
- 插入或覆盖HashEntry对象。
- 释放锁。
从上面的步骤可以看出,ConcurrentHashMap 在读写时均需要二次定位。首先定位到 Segment,之后定位到 Segment 内的具体数组下标。
一致性问题
调用Size()是统计ConcurrentHashMap的总元素数量,需要把各个Segment内部的元素数量汇总起来。但是,如果在统计 Segment 元素数量的过程中,已统计过的 Segment 瞬间插入新的元素,怎么解决一致性的问题?
大体逻辑如下:
- 遍历所有的Segment。
- 把Segment的元素数量累加起来。
- 把Segment的修改次数累加起来。
- 判断所有Segment总的修改次数是否大于上一次总的修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
- 如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
再次判断所有Segment总的修改次数是否大于上一次总的修改次数。由于已经加锁,次数一定和上次相等。
- 释放锁,统计结束。
ConcurrentHashMap有些方法需要跨段,比如 size() 和 containsValue(),它们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
JDK1.8对ConcurrentHashMap做了那些优化和改动
整体结构
1.7:Segment + HashEntry + Unsafe。
1.8: 移除Segment,通过锁住链表或红黑树的首节点(Synchronized),使锁的粒度更小,Synchronized + CAS + Node + Unsafe。
put()
1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并且最多自旋64次获取锁,超过则挂起。
1.8:由于去掉了Segment,类似HashMap,可以直接定位到桶,拿到首节点后进行判断:为空则 CAS 插入;为 -1则说明在扩容,则跟着一起扩容;else则加锁put(类似1.7)
get()
基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。
resize()
1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在 1.7 中扩容时死循环的问题,保证线程安全。
1.8:支持并发扩容,HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。
size()
1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的 Segment求和。
1.8:用baseCount来存储当前的节点个数,这就设计到baseCount并发环境下修改的问题。