Java8开始ConcurrentHashMap,为什么舍弃分段锁?

Java8开始ConcurrentHashMap,为什么舍弃分段锁?

概述
我们知道, 在 Java 5 之后,JDK 引入了 java.util.concurrent 并发包 ,其中最常用的就是
ConcurrentHashMap 了, 它的原理是引用了内部的 Segment ( ReentrantLock )
分段锁,保证在操作不同段 map 的时候, 可以并发执行, 操作同段 map 的时候,进行锁的竞争和等待。从而达到线程安全, 且效率大于synchronized。但是在 Java 8 之后, JDK 却弃用了这个策略,重新使用了 synchronized+cas。

弃用原因

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

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

新的同步方案
既然弃用了分段锁, 那么一定由新的线程安全方案,

我们来看看源码是怎么解决线程安全的呢?(源码保留了segment 代码, 但并没有使用)

put

首先通过 hash 找到对应链表过后, 查看是否是第一个object, 如果是, 直接用cas原则插入,无需加锁。

Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
    tab = initTable(); // 这里在整个map第一次操作时,初始化hash桶, 也就是一个table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果是第一个object, 则直接cas放入, 不用锁
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
        break;   

然后, 如果不是链表第一个object, 则直接用链表第一个object加锁,这里加的锁是synchronized,虽然效率不如ReentrantLock, 但节约了空间,这里会一直用第一个object为锁, 直到重新计算map大小,
比如扩容或者操作了第一个object为止。

synchronized (f) {// 这里的f即为链表的第一个object或者是树的根节点
    if (tabAt(tab, i) == f) {
        if (fh >= 0) {
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
                K ek;
                if (e.hash == hash &&
                    ((ek = e.key) == key ||
                     (ek != null && key.equals(ek)))) {
                    oldVal = e.val;
                    if (!onlyIfAbsent)
                        e.val = value;
                    break;
                }
                Node<K,V> pred = e;
                if ((e = e.next) == null) {
                    pred.next = new Node<K,V>(hash, key, value);
                    break;
                }
            }
        }
        else if (f instanceof TreeBin) { // 太长会用红黑树
            Node<K,V> p;
            binCount = 2;
            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                           value)) != null) {
                oldVal = p.val;
                if (!onlyIfAbsent)
                    p.val = value;
            }
        }
        else if (f instanceof ReservationNode)
            throw new IllegalStateException("Recursive update");
    }
}

分段锁技术是在java8以前使用的,在java8已经弃用了,更新为synchronized+cas

我想从下面几个角度讨论这个问题:

1:锁的粒度 首先锁的粒度并没有变粗,甚至变得更细了。原来每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。 原来的分段锁,锁的是一个段,大小是cap/conlevel 的桶的数量,现在只需要锁一个桶
Hash冲突
JDK1.7中,ConcurrentHashMap从过二次hash的方式(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。 扩容
JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。不是每次都要整体扩容,只需要对当前链表或者红黑树扩容,减小了扩容的粒度

为什么是synchronized,而不是ReentranLock

减少内存开销 假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
获得JVM的支持 可重入锁毕竟是API这个级别的,后续的性能优化空间很小。 synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

百度搜索爱敲代码的小庆

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

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

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

打赏作者

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

抵扣说明:

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

余额充值