上篇文章对ConcurrentHashMap在put、初始化和扩容阶段的并发控制做了介绍,并分析了其相应的源码,不过这里面还有一些细节和知识点没介绍,比如jdk 1.7的分段锁用的是ReentrantLock而jdk1.8的节点锁用的是synchronized+CAS,为什么不继续用ReentrantLock?我们知道ConcurrentHashMap的值不允许为null,而HashMap可以为null,这也是它俩的一个不同点,还有就是ConcurrentHashMap的扩容时机、什么时候转红黑树,和HashMap一样吗?本节将对以上疑问进行一一解答。
ps:以下源码都是基于jdk 1.8,若不是则会有特别说明。
先从简单的说。
一、什么时候扩容
ps:一、二两节都可以参考前面HashMap的第3讲的红黑树和第5讲的扩容。
我们知道HashMap有两个扩容时机,一个是元素个数>threshold(阈值),另一个则是链表长度>=8 && 数组长度<64,ConcurrentHashMap的扩容时机也是这两个条件。
首先看一下元素个数>阈值:
链表长度>=8 && 数组长度<64:
二、树化和退化的时机
HashMap转红黑树的时机是链表长度>=8&&数组元素个数>=64,退化为链表的时机是红黑树节点个数<=6,ConcurrentHashMap也一样。
链表长度>=8&&数组元素个数>=64:
红黑树节点个数<=6:
三、为什么jdk1.8中弃用了分段锁
上篇文章也提到过,就是ConcurrentHashMap在jdk1.7采用的是分段锁,这种机制是把哈希表分成多个段(Segment),每个段都存着类似于数组+链表的结构,并且每个段都加了ReentrantLock锁,跟HashTable相比,在并发度上确实有了很大提高,但是如果在并发比较高的时候,某些段可能成为热点段,这些热点段的竞争就会很激烈,那么自然而然的也成为了性能瓶颈。
而在jdk1.8时对其做了重大改进,采用节点锁的机制,这个大家应该比较熟悉,就是如果某个桶没有元素就用CAS操作来添加节点,否则,用synchronized锁住当前节点(桶),再次尝试put。这样锁的粒度就变小了,也就提高了并发度。
四、jdk1.8为什么用synchronized
通过第三节我们知道jdk1.8采用了CAS+sychronized的机制来保证线程安全。
CAS我们比较好理解,无锁化更轻量级,但是加锁的时候为什么不继续用ReentrantLock了?它们的区别我就不再介绍了,可以看看作者之前的文章——Java中的锁。
我们知道,大多数情况下ReentrantLock要比synchronized更好,但是也要看具体的场景,ConcurrentHashMap在jdk1.8时针对节点加锁的,所以要比jdk1.7中的分段锁的并发冲突要小很多,毕竟同一个HashMap,同时去写同一个节点的概率还是很低的。
而在并发冲突不高时,synchronized就不会频繁的升级为重量级锁,大部分情况下轻量级锁就搞定了,所以这时候,sychronized和ReentrantLock加锁的性能就可以忽略不计了。
那么我们再来看看synchronized的优势:
无需手动获取和释放锁。
JVM能够在运行时动态调整锁,比如锁粗化、锁消除等。
当获取锁失败时,synchroized可以通过自旋避免线程被挂起,而ReentrantLock会直接线程挂起。
还有就是ReentrantLock是一个对象,这个对象存储了锁的状态、控制并发控访问的状态信息等,而synchronized是利用对象头来标记实现的锁,所以ReentrantLock的内存开销也会相对大一些。
所以,综上所述,应该用synchronized。
五、ConcurrentHashMap为什么key和value不能为null
这也是与HashMap比较大的一个区别,HashMap的key和value都可以为null,但ConcurrentHashMap则都不允许为null,这是为什么呢?
这个问题我们ConcurrentHashMap的作者——并发大神Doug Lea也做出过回应,大概意思就是:
null在非Map中(如HashMap),是可以容忍模糊性(二义性)的,但在并发Map中是无法容忍的,容易造成歧义。
比如说map.get(key)返回null,那么我们无法确认是因为当时这个key的value就是null,还是说这个key根本不存在。
在HashMap中,因为它的设计就是给单线程用的,所以map.get(key)返回null的时候我们可以通过map.contains(key)来检测。
但是像ConcurrentHashMap,它是为并发而生的,当map.get(key)返回null的时候,是没办法通过map.contains(key)来检测的,因为在检测过程中可能会被其它线程修改,这就导致了检测结果并不可靠。
所以为了让ConcurrentHashMap的语义更加准确,它就不支持null。
这个问题也是Doug Lea和Josh Bloch(HashMap作者之一)在设计问题上少数持不同意见之一,而ConcurrentHashMap是Doug Lea独立开发的,所以也就禁止了null值的存在。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。