问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

ConcurrentHashMap源码解析(超级详细版本)

发布网友 发布时间:2024-09-15 07:52

我来回答

1个回答

热心网友 时间:2024-09-20 23:48

concurrentHashMap概述

concurrentHashMap是HashMap的线程安全版本,是juc包中提供的。利用volatile关键字和cas还有synchronized关键字对hashMap进行改造,使其变为线程安全的。其很多特性和hashMap是一样的,hashMap的源码可以去看我之前的博客:https://www.shouxicto.com/article/129.html

但是与hashMap区别不只是线程安全方面,还有就是concurrentHashMap不允许key和value为空,这点在后面源码解析也可以看到,本文章主要分析JDK1.8及之后的版本,1.7之前的版本会提一点,不会对源码进行详细的解析。

数据结构图解

自我感觉学一个东西首先要了解他的架构图,包括框架,这样我们可以有一个整体的框架,学习起来也会更轻松。接下来我们来看concurrentHashMap的数据结构图,下面两张图片是从别人博客里面摘的(ps:感觉很多博客都是这两张图,就不注明出处了,我:拿来吧你0.0)

1.8:

1.7:

如图,1.8版本之后,concurrentHashMap和hashMap数据结构基本是一致的,唯一的区别就是concurrentHashMap是在操作链表或者红黑树时对node数组中的每一个链表或红黑树加锁,其他时候利用cas来保证线程同步。并且利用volatile关键字,保证了table数组、标索引等的可见性,结合cas,在不加锁的情况下保证线程同步,相较于1.7,还引入了红黑树,极大的优化了速度。

1.7版本之前,concurrentHashMap是利用分段锁,将entry数组分为多个segment数组,然后当我们插入数据时对segment加锁,相较于hashTable的全部加锁速度快了不少,并且能够至此一定的并发操作,但是速度方面还是不够优秀。

源码解析1.GET方法

get方法比较简单,相对于put方法,只需要我们通过valotile关键字保证可见性就可以了,所以和hashMap差别不大

在读这get源码之前,我们需要知道一个方法,这个方法在put中也被用到,我们需要理解他是干什么的:

staticfinal<K,V>Node<K,V>tabAt(Node<K,V>[]tab,inti){return(Node<K,V>)U.getObjectVolatile(tab,((long)i<<ASHIFT)+ABASE);}

这段代码主要是用来取table数组中的对应下标的元素的,可以看到调用了一个getObjectVolatile()方法,这个方法是Usafe类提供的一个方法,这个类提供了一些原子操作,是通过我们的本地方法实现的,也就是调用了C++代码

然后是spread()方法,可以看到这个和我们hashmap中的hash()方法是一样的,只是和HASH_BITS进行了一次与运算,这个是用来使最高位为0,保证我们的哈希值为正数,代表是一个链表,若为1则为负数,代表一个红黑树,这个后面判断红黑树的时候会用到。

staticfinalintHASH_BITS=0x7fffffff;//usablebitsofnormalnodehashstaticfinalintspread(inth){return(h^(h>>>16))&HASH_BITS;}

然后我们来看get的源码:

publicVget(Objectkey){Node<K,V>[]tab;Node<K,V>e,p;intn,eh;Kek;inth=spread(key.hashCode());if((tab=table)!=null&&(n=tab.length)>0&&(e=tabAt(tab,(n-1)&h))!=null){if((eh=e.hash)==h){if((ek=e.key)==key||(ek!=null&&key.equals(ek)))returne.val;}elseif(eh<0)return(p=e.find(h,key))!=null?p.val:null;while((e=e.next)!=null){if(e.hash==h&&((ek=e.key)==key||(ek!=null&&key.equals(ek))))returne.val;}}returnnull;}

首先我们通过spread()方法计算出传入的key的hash值,如果table不为空并且(n-1)&h对应位置的节点不为空(n是table数组长度),

那么就先判断对应位置根节点的hash值是否等于我们计算出的key值的hash值,如果相等就直接返回根节点的val值,否则就进行下一步

若根接节点的hash值小于0,若小于0代表为红黑树,那么就采用红黑树的方式向下查找,找到有相同的值就返回,否则返回null。

若接节点的hash值大于等于0,遍历node链表向下找,同样找到就返回值,否则返回null。

到此,get方法结束。

2.PUT方法

put方法相对就比较麻烦了,代码如下:

publicVput(Kkey,Vvalue){returnputVal(key,value,false);}

这里调用了putVal()方法,哪个false参数是代表当put的值重复时要不要替换,false代表要替换。然后我们看putVal()方法:

finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){//判断key和value是否为空,为空直接抛出空指针异常if(key==null||value==null)thrownewNullPointerException();//计算hash值,和put方法中一致inthash=spread(key.hashCode());//用于记录链表长度intbinCount=0;for(Node<K,V>[]tab=table;;){Node<K,V>f;intn,i,fh;//如果table未初始化,就先初始化tableif(tab==null||(n=tab.length)==0)tab=initTable();//如果要插入的table数组下标位置为空,则直接利用cas插入一个node即可elseif((f=tabAt(tab,i=(n-1)&hash))==null){if(casTabAt(tab,i,null,newNode<K,V>(hash,key,value,null)))break;//nolockwhenaddingtoemptybin}//如果table数组正在扩容,那么就需要当前线程帮助其扩容elseif((fh=f.hash)==MOVED)tab=helpTransfer(tab,f);//如果下标位置有节点,并且没有在扩容else{//记录要被替换的老数据VoldVal=null;//对table数组对应下表位置的根节点加锁synchronized(f){if(tabAt(tab,i)==f){//如果根节点hash大于等于0,代表为链表if(fh>=0){binCount=1;//对链表进行遍历,binCount记录链表长度for(Node<K,V>e=f;;++binCount){Kek;//如果有相同的节点if(e.hash==hash&&((ek=e.key)==key||(ek!=null&&key.equals(ek)))){//将老数据进行保存oldVal=e.val;//根据参数条件,将节点的value值进行替换if(!onlyIfAbsent)e.val=value;break;}Node<K,V>pred=e;//如果没有相同节点,并且遍历到了最后一个节点if((e=e.next)==null){//将节点拼接在链表最后面pred.next=newNode<K,V>(hash,key,value,null);break;}}}//如果根节点是树节点,用树的方式进行插入elseif(finstanceofTreeBin){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;}}}}if(binCount!=0){//判断链表长度,如果大于TREEIFY_THRESHOLD,也就是大于8if(binCount>=TREEIFY_THRESHOLD)//尝试进行扩容treeifyBin(tab,i);//如果是替换,那么返回替换前的valueif(oldVal!=null)returnoldVal;break;}}}//添加计数,判断是否需要扩容,如果正在扩容,则帮助扩容,扩容后再次判断是否需要再次扩容。//根据sizeCtl扩容阈值(注意还有一个SIZECTL,这两个并不是同一个东西)//判断是否达到阈值需要进行扩容,需要扩容的话,//判断是否正在扩容,正在扩容则帮助扩容addCount(1L,binCount);returnnull;}

我们来看一下这个addcount()方法:

//从putVal传入的参数是1,binCount,binCount默认是0,只有hash冲突了才会大于1.且他的大小是链表的长度(如果不是红黑数结构的话)。privatefinalvoidaddCount(longx,intcheck){CounterCell[]as;longb,s;//如果计数盒子不是空或者//如果修改baseCount失败if((as=counterCells)!=null||!U.compareAndSwapLong(this,BASECOUNT,b=baseCount,s=b+x)){CounterCella;longv;intm;booleanuncontended=true;//如果计数盒子是空(尚未出现并发)//如果随机取余一个数组位置为空或者//修改这个槽位的变量失败(出现并发了)//执行fullAddCount方法。并结束if(as==null||(m=as.length-1)<0||(a=as[ThreadLocalRandom.getProbe()&m])==null||!(uncontended=U.compareAndSwapLong(a,CELLVALUE,v=a.value,v+x))){fullAddCount(x,uncontended);return;}if(check<=1)return;s=sumCount();}//如果需要检查,检查是否需要扩容,在putVal方法调用时,默认就是要检查的。if(check>=0){Node<K,V>[]tab,nt;intn,sc;//如果map.size()大于sizeCtl(达到扩容阈值需要扩容)且//table不是空;且table的长度小于1<<30。(可以扩容)while(s>=(long)(sc=sizeCtl)&&(tab=table)!=null&&(n=tab.length)<MAXIMUM_CAPACITY){//根据length得到一个标识intrs=resizeStamp(n);//如果正在扩容if(sc<0){//如果sc的低16位不等于标识符(校验异常sizeCtl变化了)//如果sc==标识符+1(扩容结束了,不再有线程进行扩容)(默认第一个线程设置sc==rs左移16位+2,当第一个线程结束扩容了,就会将sc减一。这个时候,sc就等于rs+1)//如果sc==标识符+65535(帮助线程数已经达到最大)//如果nextTable==null(结束扩容了)//如果transferIndex<=0(转移状态变化了)//结束循环if((sc>>>RESIZE_STAMP_SHIFT)!=rs||sc==rs+1||sc==rs+MAX_RESIZERS||(nt=nextTable)==null||transferIndex<=0)break;//如果可以帮助扩容,那么将sc加1.表示多了一个线程在帮助扩容if(U.compareAndSwapInt(this,SIZECTL,sc,sc+1))//扩容transfer(tab,nt);}//如果不在扩容,将sc更新:标识符左移16位然后+2.也就是变成一个负数。高16位是标识符,低16位初始是2.elseif(U.compareAndSwapInt(this,SIZECTL,sc,(rs<<RESIZE_STAMP_SHIFT)+2))//更新sizeCtl为负数后,开始扩容。transfer(tab,null);s=sumCount();}}}

x参数表示的此次需要对表中元素的个数加几。check参数表示是否需要进行扩容检查,大于等于0需要进行检查,而我们的putVal方法的binCount参数最小也是0,因此,每次添加元素都会进行检查。(除非是覆盖操作),下面是详细步骤:

判断计数盒子(counterCells)属性是否是空,如果是空,就尝试修改baseCount变量,对该变量进行加X。

如果计数盒子不是空,或者修改baseCount变量失败了,则放弃对baseCount进行操作。

如果计数盒子是null或者计数盒子的length是0,或者随机取一个位置是null,那么就对刚刚的元素进行CAS赋值。

如果赋值失败,或者满足上面的条件,则调用fullAddCount方法重新死循环插入。

这里如果操作baseCount失败了(或者计数盒子不是Null),且对计数盒子赋值成功,那么就检查check变量,如果该变量小于等于1.直接结束。否则,计算一下count变量。

如果check大于等于0,说明需要对是否扩容进行检查。

如果map的size大于sizeCtl(扩容阈值),且table的长度小于1<<30,那么就进行扩容。

根据length得到一个标识符,然后,判断sizeCtl状态,如果小于0,说明要么在初始化,要么在扩容。

如果正在扩容,那么就校验一下数据是否变化了(具体可以看上面代码的注释)。如果检验数据不通过,break。

如果校验数据通过了,那么将sizeCtl加一,表示多了一个线程帮助扩容。然后进行扩容。

如果没有在扩容,但是需要扩容。那么就将sizeCtl更新,赋值为标识符左移16位——一个负数。然后加2。表示,已经有一个线程开始扩容了。然后进行扩容。然后再次更新count,看看是否还需要扩容。

到此,我们的put方法结束了。

1.7与1.8的区别项目1.71.8同步机制分段锁,每个segment继承ReentrantLockCAS+synchronized保证并发更新数据结构数组+链表数组+链表+红黑树节点HashEntryNodeput操作多个线程同时竞争获取同一个segment锁,获取成功的线程更新map;失败的线程尝试多次获取锁仍未成功,则挂起线程,等待释放锁访问相应的bucket时,使用sychronizeded关键字,防止多个线程同时操作同一个bucket,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;更新了节点数量,还要考虑扩容和链表转红黑树总结

concurrentHashMap和hashMap相比,由于考虑了高并发,整体更加复杂,并且也更难以理解。本文只对put和get方法进行了详细的分析,其他方法相对来说比较简单,想要了解的朋友可以自己去查看源码。另外本次源码解析有错误的,欢迎各位指正,谢谢。

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
情侣文案英文高级浪漫87句 Love to the people don't wave.什么意思 gladtomeetyou怎么 gladtomeetyou.怎么回答 2016生肖猴运程 武汉买房88平方满50万落户政策 非武汉市户口在武汉市购买70平方总价50万的商品房,可以转户口吗... 我想在武汉买一套50万左右的新房子,谁能告诉我现在武昌,关山,江夏,有... 支付宝怎么开通步数授权? 总价50万能在武汉买一套两室一天的二手房吗? 如何快速通过西瓜视频认证?三种方法教您轻松审核通过 西瓜视频入口怎么用 如何看待头条视频改名西瓜视频,它和西瓜影音是什么关系? 老年脑萎缩治疗方法 脑萎缩什么原因引起的 小鸡没出生前,怎样判断鸡蛋里的小鸡是否成形? 万事都要全力以赴 包括开心 万事都要全力以赴,包括开心。适合安慰朋友吗? 全力以赴,势不可挡 上海掩隐罪往下查几人 如何判定不构成掩隐罪 掩饰隐瞒犯罪所得获利4万算情节轻微吗 掩隐罪2万一般判刑判多久 普法系列20➡️“掩饰隐瞒犯罪所得罪”了解一下 福建掩隐罪缓刑案件如何处理 黑龙江掩隐罪判缓可能性大吗 河北省掩隐罪判的重吗 灾后政府补助资金重建安置房能转让吗有没有土地证和房产证 支气管哮喘急性发作期的症状 哮喘急性发作期有哪些危害 河北玉鑫工贸有限公司怎么样? 无双大蛇能否一开始就骑赤兔马? 无双大蛇特技装备人物特技装备说明和推荐 无双大蛇中有没有绝影马,有的话怎么拿? 无双大蛇z特技怎么获得(无双大蛇z特殊组合一览) 红米手机悬浮窗设置 金钱树不发芽怎么办 金钱树不发芽的解决方法 我家养了一盆金钱树,最近发现不长了,而且叶子都黄了,谁能告诉我怎么回事... 请问一下榕树盆景生虫了,怎么办 运动品牌折扣店货源渠道有哪些 权利和权力最简单的区别 半途而废的成语故事,尽快有答案 荣耀V30用4G旗舰的钱能买到哪些5G旗舰功能? 6210船用柴油机水温高的原因 6210柴油机喷油器压力是多少?转速六百 好听的老情歌有哪些?求佛,一万个理由等! 万年县人民医院医院简介 万年县人民医院科室设置 江西省万年县人民医院做包皮手术一共需要多少钱?有医疗保险可以报销一 ... 万年县人民医院有口腔粘膜科吗?能做口腔囊肿切除手术吗?