前言
大家好, 这里是Yve菌, 今天给大家总结一些ConcurrentHashMap在jdk1.7和jdk1.8中的区别
ConcurrentHashMap在jdk1.7和1.8中的区别
1. 结构不同
ConcurrentHashMap在jdk1.7中使用的是Segment+HashEntry的结构, segment继承了ReentrantLock在结构中充当锁的角色, 一个segment中根据情况会存在多个HashEntry, 元素会以链表的方式存放在HashEntry中
在1.8中, ConcurrentHashMap放弃了Segment+HashEntry臃肿的设计, 改为使用Node+Synchronized+CAS的结构. Node的结构相当于1.7的HashEntry, 但HashEntry是内部类.使用synchronized+CAS代替segment这样锁的粒度更小了,并且不是每次都需要加锁, 只有CAS失败再加锁
2. put()方法
在1.7中因为存在segment, 所以在put时需要先通过key的rehash的高位和segment长度-1进行与运算得到对应segment下标; 再通过key的rehash值和table数组长度-1进行与运算获得对应table的下标. 当有线程在进行操作时会将segment锁住, 其他线程在要put发现segment被锁住时可以做一些put前的准备工作.
而在1.8中, 会根据key的rehash值和数组长度进行与运算得到对应下标后的first节点, 之后:
- 如果为null则尝试通过cas插入
- 如果不为null并且first.hash == -1, 说明其他线程正在扩容, 参与协助扩容
- 如果不为null并且first.hash != -1, synchronized锁住first节点, 判断为链表还是红黑树, 之后遍历插入
3. resize()方法
在1.7中每次扩容只会扩容segment内部的HashEntry, 而segment本身并不会扩容, 在需要扩容获得锁之后, 在单线程中去做扩容
而在1.8中扩容支持并发节点转移, 在线程put时发现有其他线程进行扩容时会协助节点转移, 从数组尾部开始, 如果该位置已经被处理过了就会创建一个ForwardingNode放到首节点, hash值设为-1, 这样其他线程就知道该位置被处理过了
4. 计算size
在1.7中会计算两次size, 如果两次结果一样说明是正确的, 直接返回. 如果两次结果不一样则会把所有的segment锁住之后重新计算所有segment的count总和.
在1.8中由于没有segment的概念, 所以存在一个baseCount来记录节点个数, 如果出现多线程竞争的情况开启一个countercells数组, 其他线程记录变化的值会被加载到对应的coutercell位置中, 最后统计baseCount和countercells的总和.