当前位置: 首页 > 后端技术 > Java

【JUC】ConcurrentHashMap原理分析(一)

时间:2023-04-01 20:25:11 Java

一、新方法java1.8之后,HashMap提供了一些新方法,方便一些特定场景的操作。compute//当key的值不存在时,执行computeIfAbsent(key,k->{//业务代码returnxxxx;})//执行computeIfPresent(key,(k,v)->{//业务代码returnxxxx;})//根据传入函数计算key的值compute(key,(k,v)->{//业务代码returnxxxx;})使用示例:ConcurrentHashMapchm=newConcurrentHashMap<>();@TestpublicvoidtestCompute()throwsInterruptedException{Stringkey="test";for(inti=0;i<10;i++){newThread(()->{//通过compute方法累加某个key的值chm.compute(key,(k,v)->{if(v==null){v=10;}else{v+=10;}returnv;});}).start();}TimeUnit.SECONDS.sleep(2L);System.out.println(chm.get(key));}merge//合并key相同的值,函数的两个参数分别代表旧值和新值merge(key,(oldVal,newVal)->{//业务代码返回xxxx;})二、实现原理1、初始化表数组我们先从put方法开始分析首先会进入初始化表数组逻辑publicVput(Kkey,Vvalue){returnputVal(key,value,false);}transientvolatileNode[]table;finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){//...省略...for(Node[]tab=table;;){//==1.初始化tabif(tab==null||(n=tab.length)==0){tab=initTable();}//...省略...//##sizeCtl变量具有状态机的功能//--1,=-1,tab正在初始化(或展开);//--2,>0,表示扩展因子//--3,<-1,用于计算参与扩展的线程数privatetransientvolatileintsizeCtl;privatefinalNode[]initTable(){Node[]tab;国际标准委员会;while((tab=table)==null||tab.length==0){//`<0`表示其他线程正在初始化tab,当前线程尝试放弃对cpu的控制//(再次进入循环,选项卡可能已经初始化)if((sc=sizeCtl)<0)Thread.yield();//修改cas模式下sizeCtl的值,改为-1表示初始化;elseif(U.compareAndSwapInt(this,SIZECTL,sc,-1)){try{//这里使用了双重检查如果((tab=table)==null||tab.length==0){intn=(sc>0)?sc:默认容量;节点[]nt=(节点[])新节点[n];表=标签=nt;//计算膨胀因子的值以达到`3/4尺寸`膨胀sc=n-(n>>>2);}}finally{//设置为正数,表示扩展因子sizeCtl=sc;}休息;}}returntab;}2、普通put的spread()方法计算出来的hash必须是正数。散列的负数有特殊含义staticfinalintMOVED=-1;//迁移staticfinalintTREEBIN=-2;//树//##哈希值计算,必须为正数;复数有特殊含义(见上面的属性)staticfinalintspread(inth){//int是32位//`h^(h>>>16)`//正数:高16位和0异或;负数:高16位与1异或(同0异1)//低16位与高16位异或,增加随机性//以上结果加到`&HASH_BITS`,即`&0x7fffffff`=&01111111..(后面都是1)//结果必须是正数(最高位0表示正数)return(h^(h>>>16))&HASH_BITS;}通过(n-1)&hash计算落点bucket如果bucket为空则新建一个Node并替换为cas如果bucket中有元素则先锁住head元素,然后根据到元素类型看下一个元素是链表情况:遍历链表,如果找到key相同的元素,则替换;如果没有找到,则新建一个Node尾插入链表finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){//##key和val不能为空if(key==null||value==null)throw新的NullPointerException();//##哈希值计算inthash=spread(key.hashCode());intbinCount=0;for(Node[]tab=table;;){Nodef;诠释n,我,fh;//...省略初始化逻辑...//==1.桶中没有节点,以cas模式创建elseif((f=tabAt(tab,i=(n-1)&hash))==null){if(casTabAt(tab,i,null,newNode(hash,key,value,null)))中断;//添加到空bin时不加锁}//...省略...//==2.桶对应的节点有值的情况(也分链和红黑树)else{V旧值=空值;//##锁定bucket指向的Node//在之前的逻辑中:i-hash计算落点bucket的index;f-桶i的Node对象;fh-f对象值synchroniz的散列ed(f){//double-checkif(tabAt(tab,i)==f){//--f对象的hash值大于等于0,表示链if(fh>=0){binCount=1;for(Nodee=f;;++binCount){Kek;//--在链上找到具有相同键的元素,替换值if(e.hash==hash&&((ek=e.key)==key||(ek!=null&&key.equals(ek)))){oldVal=e.val;如果(!onlyIfAbsent)e.val=值;休息;}节点pred=e;//--控制链中节点的移动;如果在链上没有找到具有相同键的节点,则该节点将被插入到末尾if((e=e.next)==null){pred.next=newNode(hash,key,value,null);休息;}}}//--如果节点是树elseif(finstanceofTreeBin){//...省略。..}}}//...省略...3.put触发扩容1.当其中一个扩容条件满足其中一个条件时,会触发链上元素>=8,且元素个数<64.元素个数超过膨胀因子,在膨胀前会计算膨胀戳,膨胀戳的计算和元素个数有关,也会和sizeCtl有关。由此,可以得出两个结论。展开时sizeCtl必须为负数;如果sizeCtl-2=rs<<16,表示扩容超过2,数据迁移扩容会引起bucket数据迁移。每个线程负责迁移一定数量的桶。上图中,线程A被分配到16-31号桶进行迁移;之后线程B被分配到0-15的buckets帮助迁移;而线程C进入,试图帮助迁移(helpTransfer方法)——由于bucket已经被迁移线程划分(图中的情况),没有帮助线程C会直接将数据放入新tab的bucket中。如果落点桶为空,则将一个转发节点(fwd)放入桶中,fwd的nextTable属性指向新表。如果落点桶是树(忽略)如果落点桶是链,则不能写下来。下一篇继续分析(下一篇包括链式迁移原理、元素计数、get方法等)