ConcurrentHashMap第2讲——剩下的细节

上篇文章对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:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橡 皮 人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值