ConcurrentHashMap扩容示意图(JDK 1.8版本) - omigaw/spring- GitHub Wiki

1. ConcurrentHashMap简介

ConcurrentHashMap在JDK1.6的版本,关键要素有两点:
* segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障。
* segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。
而到了JDK 1.8的ConcurrentHashMap就有了很大的变化光是代码量就足足增加了很多。1.8版本舍弃了segment,并且大量使用了synchronized,以及CAS无锁操作以保证ConcurrentHashMap操作的线程安全性。至于为什么不用ReentrantLock而是Synchronized呢?实际上,synchronized做了很多的优化,包括偏向锁、轻量级锁、重量级锁、可以依次向上升级锁状态,但不能降级,因此,使用synchronized相较于ReentrantLock的性能会持平甚至在某些情况更优,底层数据结构改变为采用数组+链表+红黑树的数据形式。

2. 总结

JDK6、7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度,分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,使用volatile来保证可见性。
1.8之前put定位节点时要先定位到具体的segment,然后再在segment中定位到具体的桶。而在1.8的时候摒弃了segment臃肿的设计,直接针对的是Node[] table数组中的每一个桶,进一步缩小了锁粒度。并且防止拉链过长导致性能下降,当链表长度大于8的时候采用红黑树的设计。

3. JDK1.8中ConcurrentHashMap优化

  • spread()重哈希,以减小hash冲突 该方法主要将key的hashCode的低16位与高16位进行异或运算,这样不仅能够使得hash值能够分散能够均匀减小hash冲突的概率,另外只用到了异或运算,在性能开销上也能兼顾,做到平衡的trade-off。
  • ConcurrentHashMap的大小为2的幂次方 (n-1)&hash运算等价于对长度n取模,也就是hash%n,但是位运算比取模运算的效率要高很多。
  • 为啥要是2的幂次方 承接上面,2的幂次方就是保证每次扩容rehash只需要判断hashcode前面的一个字符是0还是1就行。

4. ConcurrentHashMap中头插尾插问题

1.7采用头插法 
1.8采用尾插法

5. HashMap 并发情况下产生的死循环问题

两个线程同时进行rehash过程中,一个线程cpu时间片用完停止了,另一个线程执行rehash过程,将原有链表倒转了,导致初始的线程rehash过程中产生死循环。