1. ConcurrentHashMap1.7和1.8版本实现原理
-
JDK1.7分段锁:1.7中,ConcurrentHashMap是由一个Segment数组和多个HashEntry数组组成,Segment数组中每一个元素存储的是HashEntry数组和链表,当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的Segment锁。多个线程可以同时访问不同的Segment,从而使并发度提高,并发度与 Segment 的数量相等
-
JDK1.8Synchronized 和 CAS:1.8中,采用Node数组+链表+红黑树的数据结构来实现,并发控制改成使用 Synchronized 和 CAS 操作,对每个Node加锁,相比于1.7锁粒度更细。(插入元素时,如果桶为空,则cas插入元素,如果桶不为空,且当前该节点不处于移动状态,那么对该节点加synchronized锁,如果该节点hash>=0,则遍历链表更新节点或者插入新节点;如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点)
-
1.8开始采用尾插法,引入了红黑树。
2. ConcurrentHashMap有什么特点?(为什么保证线程安全且速度更快)
ConcurrentHashMap1.8中是对每个Node加锁,并发控制使用Synchronized、volatile和CAS来操作,其中volatile,CAS等无锁技术可以减少锁竞争对于性能的影响,在线程安全的基础上提供了更好的写并发能力,和HashMap比更安全,和HashTable相比更高效。
3. ConcurrentHashMap的get过程?
首先计算hash值,定位到数组中桶的位置,再往下查找,整个get过程中不需要加锁,因为HashEntry 中的 val 属性用 volatile 关键词修饰,能够在线程之间保持可见性,所以每次获取时都是最新值,能够被多线程同时读,保证了线程安全和高效。
4. ConcurrentHashMap是怎么添加节点的?分1.7和1.8
- 1.7:首先对要添加节点进行hash计算,定位到Segment,再通过一次hash计算,定位到HashEntry数组中的索引位置,遍历索引位置的链表,如果有节点存在相同的key,就替换该节点的value,没有的话,就将新节点插入到链表头部。
- 1.8:对要添加节点进行hash计算,定位到数组索引位置,遍历索引位置的链表,如果有节点存在相同的key,就替换该节点的value,没有的话,就将新节点插入到链表尾部。
这里判断节点key是否相同,是先判断hash值,再用equals判断,两者都相等则表示key相等,这也是重写equals方法时也需要重写hashCode方法的原因。
5. ConcurrentHashMap的put过程?
- key或value如果为空,抛空指针异常
- table如果为空或length==0,初始化table
- 首先计算key的hash值,对应到桶的位置,如果桶为空,那就利用cas操作将节点加入到桶中。
- 如果桶不为空,且当前该节点不处于移动状态,那么对该节点加synchronized锁,如果该节点hash>=0,则遍历链表更新或插入节点;
- 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点
- 如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树.
- 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount,并且检查是否需要扩容。
6. ConcurrentHashMap怎么计算size?
- HashMap 定义了一个 modCount 变量,每次变动时,无论是 put 还是 remove ,都将 modCount 加1。遍历两次数组,如果得出的 modCount 值一样,就表示未变动了,成功返回 size。否则就表示又变动过了,就继续遍历再次比较 modCount。
- ConcurrentHashMap 1.7:首先以不加锁的方式,定义了一个 modCount 变量,每次变动时,无论是 put 还是 remove ,都将 modCount 加1。遍历两次数组,如果得出的 modCount 值一样,就表示未变动了,成功返回 size。否则就再遍历一次,如果依然不一致,就对所有的 Segment 加锁,然后一个一个遍历,准确的求出 size。
(ConcurrentHashMap在求size时首先会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,如果一致就认为当前没有元素加入,计算的结果是准确的,然后返回结果。如果第一种方案不符合,他就会给每个Segment加上锁,然后计算size并返回。) - ConcurrentHashMap 1.8:新值了 mappingCount 方法,它的返回值类型是 long 类型,不会因为 size 方法是 int 类型而限制最大值。它里面有一个 volatile 修饰的 baseCount 变量,当没有发生冲突时,使用 baseCount 来计数, 还有一个填充单元 CounterCell 数组,当并发产生冲突时,使用 CounterCell 计数,最后通过 baseCount 和 CounterCell 数组总的计数值来得到 size 大小。其中 CounterCell 添加了 @Contended 注解来防止伪共享,伪共享产生的原因是因为缓存系统是以缓存行为单位进行存储的,缓存行是 2 的幂次方的连续字节,一般为64个字节,当多线程修改同一个缓存行中不同的变量时,由于同时只能有一个线程操作缓存行,所以会影响彼此的性能。JDK1.8 通过添加注解 @Contended 使变量在缓存行中分隔开来解决伪共享问题。(在JDK1.8之前是通过数据填充的方式来解决)(得出的 size 并不准确)
(1.8中使用一个volatile类型的变量baseCount和一个CounterCell数组记录元素的个数,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数。
为啥有baseCount和CounterCell?
在一个低并发的情况下,就只是简单地使用CAS操作来对baseCount进行更新,但只要这个CAS操作失败一次,就代表有多个线程正在竞争,那么就转而使用CounterCell数组进行计数,数组内的每个ConuterCell都是一个独立的计数单元。)