ConcurrentHashMap在Java 7和Java 8中的区别
ConcurrentHashMap在Java中一直是一个多线程的大杀器。在多线程环境下,使用它就可以免去使用HashMap造成的线程不安全的问题。鉴于Java 8已经出来N久了,在各个方面和Java 7都有所不同。下面我们就从源码级别对ConcurrentHashMap进行分析,看看从Java 7 到Java 8,它到底有哪些变化?
Java 7中的ConcurrentHashMap
put方法实现
在Java 7 中,put一个值到ConcurrentHashMap主要有以下这些步骤:
- 首先计算出这个key对应的segment,
(hash >>> segmentShift) & segmentMask;
- 通过key再次计算出,这个值在table数组中的位置
- 首先尝试获取锁,成功获取锁之后,转到4,否则,转到8
- 获取table数组的头结点,遍历;
- 如果当前key在链表中,则直接覆盖其值,不在的话,转为6
- 将节点插入头结点位置,作为第一个节点;
- 判断是否需要扩容,如有,扩容
- 遍历是否存在相同的key
- 在步骤8遍历时,自旋获取锁,如果超过最大限制64次,则阻塞。
put方法
1 | final V put(K key, int hash, V value, boolean onlyIfAbsent) { |
scanAndLockForPut
在不超过最大重试次数MAX_SCAN_RETRIES通过CAS尝试获取锁
1 | private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) { |
size方法实现
在Java 7 中求ConcurrentHashMap的方式如下步骤:
- 先不加锁,计算两次ConcurrentHashMap的大小,如果两次结果是一样的,说明是正确的,返回
- 如果两次结果不一样,则锁住所有的segment,重新计算所有segment的count之和。
size方法
1 | public int size() { |
Java 8中的ConcurrentHashMap
数据结构
Java 8 中去除了Java 7 的segment分段锁,转而使用Node+synchronized+CAS来保证并发安全。
put方法实现
ConcurrentHashMap在Java 8 的put步骤如下:
- 根据key的hash值定位到table的首节点first;
- 如果为null,新增节点,通过cas的方式;
- 如果不为null,并且,first.hash == -1,说明其他线程正在扩容,参与一起扩容
- 如果不为null,并且first.hash != -1,Synchronized锁住first节点,判断是链表还是红黑树,遍历插入
size方法实现
由于没有segment的概念,所以只需要用一个 baseCount
变量来记录ConcurrentHashMap 当前 节点的个数
。
- 先尝试通过CAS 修改
baseCount
- 如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将
CELLSBUSY
置1,成功则可以把baseCount变化的次数
暂存到一个数组counterCells
里,后续数组counterCells
的值会加到baseCount
中。 - 如果
CELLSBUSY
置1失败又会反复进行CASbaseCount
和 CAScounterCells
数组
N句话总结
- 去除
Segment + HashEntry + Unsafe
的实现,
改为Synchronized + CAS + Node + Unsafe
的实现
其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。
用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了再加锁。 - put()方法中 初始化数组大小时,1.8不用加锁,因为用了个
sizeCtl
变量,将这个变量置为-1,就表明table正在初始化。