ConcurrentHashMap增删扩容的解读

本文深入解析了Java ConcurrentHashMap在JDK1.8及以后版本的实现原理,包括put、remove操作的流程,以及在扩容过程中的并发控制。文章指出,ConcurrentHashMap在1.8后取消了段的概念,改为对桶加锁,提高了并发性能。在并发修改同一桶时,其设计确保了状态的一致性,避免了异常情况。在扩容过程中,通过helpTransfer()、addCount()和transfer()等函数协同工作,保证了读写服务的正确性和效率。
摘要由CSDN通过智能技术生成

前言

太久没看jdk,今天面试被问到ConcurrentHashMap(下面可能会简称为chm)的扩容原理,只能含糊的答出来一些,翻了翻自己的笔记,似乎当时看了(而且印象里之前看的时候就感觉网上讲得很清晰的文章很难找),但是也没有特别地整理这一块的内容。因此这里特地对它进行整理。主要包括put、get、扩容。

jdk1.8之前和之后的区别

之前看《java并发艺术》这本书的时候,对chm的印象是,将一个map划分为16个段,并发修改时只会对各自段加锁,这样就可以大大提高并发性能了。

jdk1.8之后就不是这样了,一个最重要的区别就是,取消了段的概念,而是直接对桶加锁。直观地想一下,这样的并发性能肯定会更好嘛。其它区别我就先不提了,那是真八股文。我们只对jdk1.8及之后的hashmap做一个分析,我的jdk版本是11.0.8(其实最重要的是大版本为11),1.8之后的各个版本可能略有不同,但是原理应该都类似。

分界线

下面我们就要正式开始讲原理和代码了。在这之前,我们先带着一些问题来阅读,可能会更有目标一些:

  • 没有扩容的情况下

    • 并发修改(put函数)同一个桶,会不会出现异常情况(指使chm处在一个不一致的状态,比如链表有环)?
    • 一边改,一边读,会不会出现异常情况(读会不会死循环)?
  • 扩容过程

    • 什么时候开始扩容?

    • 扩容时,读、写服务会中断/阻塞吗?和不扩容时有什么区别呢?

那从这里开始看代码吧~

put&remove函数

这俩函数是对表进行修改的函数,也是分析的重中之重

put函数流程解析

在真正开始之前,我们先回忆一下,普通的HashMap是怎么做put的

  • 计算key的hashcode,并对桶数量取余,得到桶的下标
  • 如果当前的桶是空的,就为它创建一个新的桶链表头,结束插入流程;否则在已有的桶链表/树中,查找是否已经存在这个key
    • 若存在key,则更新它的value
    • 不存在,则插入新节点。顺便还会在链表超出设定范围的时候,执行一次“树化”

chm不过是HashMap的升级版,大致流程是类似的,只不过对并发访问的正确性和效率做了针对性处理。我们首先将其进行拆解,并且忽略一些不重要的因素,得到一个简化版的函数

public class ConcurrentHashMap<K,V> 
    extends AbstractMap<K,V> 
    implements ConcurrentMap<K,V>, Serializable {
   
    
    public V put(K key, V value) {
   
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
   
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 这个死循环的意义在于,为了尽量避免同步锁(阻塞)、加快扩容速度,chm会根据当前桶的情况做不同的操作。循环内是多个if-elif-else语句块,每次循环只会执行其中的一块逻辑,因此总体还是清晰的
        for (Node<K,V>[] tab = table;;) {
   
            Node<K,V> f; int n, i, fh; K fk; V fv;
            
            // 当数据结构还未初始化,就使用CAS初始化,并且从头开始循环
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            
            // 当桶为空,cas设置桶,如果设置成功,put操作就算完成了,否则从头开始循环
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
   
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;
            }
            
            // 如果当前处于扩容过程,那就为扩容尽一份力。扩容完成后会从头开始循环
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);

            // 这里是真正地,对已存在的桶加锁并处理的情况
            else {
   
                synchronized (f) {
   
                    // 首先要判断,锁住的这个桶是否和之前获取的一样(因为可能会出现这个节点被删掉/删掉之后又插入的情况,那就锁了个“假桶”),如果不一样,再次回到循环开头处理
                    if (tabAt(tab, i) == f) {
   
                        // 忽略这段又臭又长的代码,和HashMap基本一样
                    }<
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值