ConcurrentHashMap常用问题分析

639 阅读4分钟

ConcurrentHashMap

ConcurrentHashMap使用什么技术来保证线程安全?

大部分操作和HashMap是相同的,例如初始化,扩容和链表向红黑树的转变等。

jdk1.7:Segment+synchronized+HashEntry+Unsafe来进行实现的;

jdk1.8:放弃了Segment臃肿的设计,采用Node+CAS+Synchronized+Unsafe来保证线程安全。

ConcurrentHashMap为什么用了CAS还要用synchronized?

ConcurrentHashMap使用这两种手段来保证线程安全主要是一种权衡的考虑,在某些操作中使用synchronized和CAS,主要是根据锁竞争程度来判断的。

比如:在putVal中,如果计算出来的hash槽没有存放元素,那么就可以直接使用CAS来进行设置值,这是因为在设置元素的时候,因为hash值经过了各种扰动后,造成hash碰撞的几率较低,那么我们可以预测使用较少的自旋来完成具体的hash落槽操作。

当发生了hash碰撞的时候说明容量不够用了或者已经有大量线程访问了,因此这时候使用synchronized来处理hash碰撞比cas效率要高,因为发生了hash碰撞大概率来说是线程竞争比较强烈。

ConcurrentHashMap的get方法是否要加锁,为什么?

不需要,get方法采用了unsafe也就是CAS机制来保证获取到的元素是最新的元素,即采用了volatile来保证集合可见,来保证线程安全。

ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?

弱一致性(fail-safe),hashmap强一致性(fail-fast)。

ConcurrentHashMap可以支持在迭代过程中,向map添加新元素,而HashMap则抛出了ConcurrentModificationException,因为HashMap包含一个修改计数器,当你调用他的next()方法来获取下一个元素时,迭代器将会用到这个计数器。

ConcurrentHashMap1.7和1.8的区别?

jdk1.8的实现降低锁的粒度,jdk1.7锁的粒度是基于Segment的,包含多个HashEntry,而jdk1.8锁的粒度就是Node。

数据结构:jdk1.7 Segment+HashEntry;jdk1.8数组+链表+红黑树+CAS+synchronized

ConcurrentHashMap和HashMap最大容量的不一致

HashMap最大容量只能达到Intger.MAX_VALUE=2^30超过了就不再扩容而ConcurrentHashMap额外提供了mappingCount()方法最大可以达到2^63-1。

ConcurrentHashMap和HashMap转换为红黑树的条件

HashMap当某个槽的链表长度大于8就会转换为红黑树

ConcurrentHashMap当某个槽个数增加到超过了8且table大于等于64才会转换为红黑树,当table容量小于64的时候,只会扩容,不会转换为红黑树。

取决于:MIN_TREEIFY_CAPACITY 值

ConcurrentHashMap size()方法

ConcurrentHashMap只能返回一个大概数量无法做到100%精确的,因为统计过的槽在size()返回最终结果可能会出现了变化,如果只是为了统计而阻塞增删有点得不偿失。

JAVA8的 ConcurrentHashMap 为什么放弃了分段锁?

jdk8 放弃了分段锁而是用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。

通过  JDK 的源码和官方文档看来, 他们认为的弃用分段锁的原因由以下几点:

  • 加入多个分段锁浪费内存空间。
  • 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
  •   为了提高 GC 的效率。

但是ConcurrentHashMap的一些操作使用了synchronized锁,而不是ReentrantLock,虽然说jdk8的synchronized的性能进行了优化,但是我觉得还是使用ReentrantLock锁能更多的提高性能。

为什么是synchronized,而不是ReentranLock

  1. 减少内存开销

假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

  1. 获得JVM的支持

可重入锁毕竟是API这个级别的,后续的性能优化空间很小。

synchronized则是JVM直接支持的,JVM能够在运行时做出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。