ConcurrentHashMap的前世今生
1 JDK1.7下的ConcurrentHashMap
- 在JDK1.7之前的ConcurrentHashMap采用的是分段锁的思想,支持并发操作,所以是线程安全的。ConcurrentHashMap在内部划分成了若干个数据段(Segment),可以把每一段都大致理解成是一个HashMap。默认Segment大小为16,Segment的个数也就是锁的并发度。
- ConcurrentHashMap是由Segment数组和HashEntry数组组成的,HashEntry数组就是HashMap中的哈希桶数组,每个Segment元素中都存储一个HashEntry数组。HashEntry数组与JDK1.7以前的HashMap中的相同,是数组+链表的结构。
- ConcurrentHashMap对数据操作时,会对Segment段加锁(ReentrantLock ),先通过hash运算查询到数据所在的Segment段,然后对该Segment进行加锁后存储,这样就允许多个线程同时对不同Segment段的数据进行读写而不会产生线程不安全的状况,提高了并发度。
- 统计长度时,会先不加锁统计两次,如果一样即为长度,否则加锁,重新统计。先采用不加锁的方式,连续计算元素的个数,最多计算3次:如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
2 JDK1.8下的ConcurrentHashMap
JDK1.8的HashMap中比JDK1.7多增加了一个红黑树来增加数据分布的平衡性(ConcurrentHashMap也一样)。JDK1.8的ConcurrentHashMap相对于JDK1.7,用synchronized+CAS代替了分段锁,锁的粒度变小了,并发性更大了。
- 锁的粒度
JDK1.8中ConcurrentHashMap用synchronized+CAS代替了分段锁,Segment锁的粒度是Segment的个数,而JDK1.8中只会HashEntry的首节点进行加锁,所以粒度应该是下降了; - 采用synchronized+CAS代替了ReentrantLock分段锁的原因
- 分段锁中加入了Segment数组,Segment数组需要占用更多的内存空间;
- 基于JVM的synchronized经过不断优化,性能不比ReentrantLock差,而且由于是JVM级别的,会随着JVM的优化不断被优化,提升空间比API级别的ReentrantLock更大;
- size实现更简单了,用Volatile修饰baseCount以记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount
3 JDK1.8下的put方法
- 初始化操作:计算hash值,定位数组下标等;
- 如果待插入位置没有节点元素,则通过CAS的方式,插入节点;
- 若当前节点Hash值为正在转移,则调用helpTransfer方法方法帮助转移,如果还在扩容,则先进行扩容;
- 如果发生Hash碰撞,则用synchronized关键字锁定当前节点,再进入插入(链表或红黑树插入)。
注意:当链表长度大于等于8时,若哈希桶数组长度length<64,则不进行链表转成红黑树,而是直接进行2倍扩容
4 JDK1.8下的get方法
ConcurrentHashMap中的get方法没有加锁,因为Node属性的val和next,是用volatile修饰的,保证了可见性。
Node类:
static class Node<K,V> implements Entry<K,V> {