1.HashMap 的扩扩容初始值和最大值和扩容因子
2.HashMapå®ç°åç
3.concurrenthashmap1.8源码如何详细解析?
4.为什么HashMap是线程不安全的
5.结合源码探究HashMap初始化容量问题
HashMap 的初始值和最大值和扩容因子
HashMap 初始化默认值为。你可以通过构造函数自定义初始值。容源
最大值为1<<,源码这个值表示2的分析次方。在HashMap的扩扩容源码注释中有明确说明。
理解左移操作符<<是容源峰游金花源码关键,它执行二进制左移操作。源码例如,分析1 << x 等同于2的扩扩容x次方。
当存储元素超过最大值时,容源HashMap会强制将数组大小capacity设置为最大值。源码
初始化和扩容时,分析数组大小capacity被限制在两个地方:通过tableSizeFor()函数设置为2的扩扩容幂次,不超过最大值;或在容量翻倍时,容源设置为1 << ,源码但实际容量为Integer.MAX_VALUE避免整型溢出。
加载因子,即扩容因子,决定何时进行扩容。比如,加载因子为0.5,初始化容量为时,当元素数达到8个,HashMap会进行扩容。arpg项目 源码加载因子为0.时,考虑性能与容量平衡。
以上参数在JDK源代码中定义,是使用HashMap的基础。
HashMapå®ç°åç
HashMapå¨å®é å¼åä¸ç¨å°çé¢çé常é«ï¼é¢è¯ä¸ä¹æ¯çç¹ãæ以å³å®åä¸ç¯æç« è¿è¡åæï¼å¸æ对æ³çæºç ç人起å°ä¸äºå¸®å©ï¼çä¹åéè¦å¯¹é¾è¡¨æ¯è¾çæã以ä¸é½æ¯æèªå·±çç解ï¼æ¬¢è¿è®¨è®ºï¼åçä¸å¥½è½»å·ã
HashMapä¸çæ°æ®ç»æ为æ£å表ï¼åååå¸è¡¨ãå¨è¿éæä¼å¯¹æ£å表è¿è¡ä¸ä¸ªç®åçä»ç»ï¼å¨æ¤ä¹åæ们éè¦å å顾ä¸ä¸ æ°ç»ãé¾è¡¨çä¼ç¼ºç¹ã
æ°ç»åé¾è¡¨çä¼ç¼ºç¹åå³äºä»ä»¬åèªå¨å åä¸åå¨ç模å¼ï¼ä¹å°±æ¯ç´æ¥ä½¿ç¨é¡ºåºåå¨æé¾å¼åå¨å¯¼è´çãæ 论æ¯æ°ç»è¿æ¯é¾è¡¨ï¼é½æææ¾ç缺ç¹ãèå¨å®é ä¸å¡ä¸ï¼æ们æ³è¦çå¾å¾æ¯å¯»åãå é¤ãæå ¥æ§è½é½å¾å¥½çæ°æ®ç»æï¼æ£å表就æ¯è¿æ ·ä¸ç§ç»æï¼å®å·§å¦çç»åäºæ°ç»ä¸é¾è¡¨çä¼ç¹ï¼å¹¶å°å ¶ç¼ºç¹å¼±åï¼å¹¶ä¸æ¯å®å ¨æ¶é¤ï¼
æ£å表çåæ³æ¯å°keyæ å°å°æ°ç»çæ个ä¸æ ï¼ååçæ¶åéè¿keyè·åå°ä¸æ ï¼indexï¼ç¶åéè¿ä¸æ ç´æ¥ååãé度æå¿«ï¼èå°keyæ å°å°ä¸æ éè¦ä½¿ç¨æ£åå½æ°ï¼åååå¸å½æ°ã说å°åå¸å½æ°å¯è½æ人已ç»æ³å°äºï¼å¦ä½å°keyæ å°å°æ°ç»çä¸æ ã
å¾ä¸è®¡ç®ä¸æ 使ç¨å°äºä»¥ä¸ä¸¤ä¸ªå½æ°ï¼
å¼å¾æ³¨æçæ¯ï¼ä¸æ 并ä¸æ¯éè¿hashå½æ°ç´æ¥å¾å°çï¼è®¡ç®ä¸æ è¿è¦å¯¹hashå¼åindex()å¤çã
Psï¼å¨æ£å表ä¸ï¼æ°ç»çæ ¼åå«å桶ï¼ä¸æ å«å桶å·ï¼æ¡¶å¯ä»¥å å«ä¸ä¸ªkey-value对ï¼ä¸ºäºæ¹ä¾¿ç解ï¼åæä¸ä¼ä½¿ç¨è¿ä¸¤ä¸ªåè¯ã
以ä¸æ¯åå¸ç¢°æç¸å ³ç说æï¼
以ä¸æ¯ä¸æ å²çªç¸å ³ç说æï¼
å¾å¤äººè®¤ä¸ºåå¸å¼ç碰æåä¸æ å²çªæ¯åä¸ä¸ªä¸è¥¿ï¼å ¶å®ä¸æ¯çï¼å®ä»¬çæ£ç¡®å ³ç³»æ¯è¿æ ·çï¼hashCodeåç碰æï¼åä¸æ ä¸å®å²çªï¼èä¸æ å²çªï¼hashCode并ä¸ä¸å®ç¢°æ
ä¸ææå°ï¼å¨jdk1.8以åHashMapçå®ç°æ¯æ£å表 = æ°ç» + é¾è¡¨ï¼ä½æ¯å°ç®å为æ¢æ们è¿æ²¡æçå°é¾è¡¨èµ·å°çä½ç¨ãäºå®ä¸ï¼HashMapå¼å ¥é¾è¡¨çç¨æå°±æ¯è§£å³ä¸æ å²çªã
ä¸å¾æ¯å¼å ¥é¾è¡¨åçæ£å表ï¼
å¦ä¸å¾æ示ï¼å·¦è¾¹çç«æ¡ï¼æ¯ä¸ä¸ªå¤§å°ä¸ºçæ°ç»ï¼å ¶ä¸åå¨çæ¯é¾è¡¨ç头ç»ç¹ï¼æ们ç¥éï¼æ¥æé¾è¡¨ç头ç»ç¹å³å¯è®¿é®æ´ä¸ªé¾è¡¨ï¼æ以认为è¿ä¸ªæ°ç»ä¸çæ¯ä¸ªä¸æ é½åå¨çä¸ä¸ªé¾è¡¨ãå ¶å ·ä½åæ³æ¯ï¼å¦æåç°ä¸æ å²çªï¼ååæå ¥çèç¹ä»¥é¾è¡¨çå½¢å¼è¿½å å°åä¸ä¸ªèç¹çåé¢ã
è¿ç§ä½¿ç¨é¾è¡¨è§£å³å²çªçæ¹æ³å«åï¼æé¾æ³ï¼åå«é¾å°åæ³ï¼ãHashMap使ç¨çå°±æ¯æé¾æ³ï¼æé¾æ³æ¯å²çªåç以åç解å³æ¹æ¡ã
Qï¼æäºæé¾æ³ï¼å°±ä¸ç¨æ å¿åçå²çªåï¼
Aï¼å¹¶ä¸æ¯ï¼ç±äºå²çªçèç¹ä¼ä¸åçå¨é¾è¡¨ä¸è¿½å ï¼å¤§éçå²çªä¼å¯¼è´å个é¾è¡¨è¿é¿ï¼ä½¿æ¥è¯¢æ§è½éä½ãæ以ä¸ä¸ªå¥½çæ£å表çå®ç°åºè¯¥ä»æºå¤´ä¸åå°å²çªåççå¯è½æ§ï¼å²çªåççæ¦çååå¸å½æ°è¿åå¼çååç¨åº¦æç´æ¥å ³ç³»ï¼å¾å°çåå¸å¼è¶ååï¼å²çªåççå¯è½æ§è¶å°ã为äºä½¿åå¸å¼æ´ååï¼HashMapå é¨åç¬å®ç°äºhash()æ¹æ³ã
以ä¸æ¯æ£å表çåå¨ç»æï¼ä½æ¯å¨è¢«è¿ç¨å°HashMapä¸æ¶è¿æå ¶ä»éè¦æ³¨æçå°æ¹ï¼è¿éä¼è¯¦ç»è¯´æã
ç°å¨æä»¬æ¸ æ¥äºæ£å表çåå¨ç»æï¼ç»å¿ç人åºè¯¥å·²ç»åç°äºä¸ä¸ªé®é¢ï¼Javaä¸æ°ç»çé¿åº¦æ¯åºå®çï¼æ 论åå¸å½æ°æ¯å¦ååï¼éçæå ¥å°æ£å表ä¸æ°æ®çå¢å¤ï¼å¨æ°ç»é¿åº¦ä¸åçæ åµä¸ï¼é¾è¡¨çé¿åº¦ä¼ä¸æå¢å ãè¿ä¼å¯¼è´é¾è¡¨æ¥è¯¢æ§è½ä¸ä½³ç缺ç¹åºç°å¨æ£å表ä¸ï¼ä»è使æ£å表失å»åæ¬çæä¹ã为äºè§£å³è¿ä¸ªé®é¢ï¼HashMapå¼å ¥äºæ©å®¹ä¸è´è½½å åã
以ä¸æ¯åæ©å®¹ç¸å ³çä¸äºæ¦å¿µå解éï¼
Psï¼æ©å®¹è¦éæ°è®¡ç®ä¸æ ï¼æ©å®¹è¦éæ°è®¡ç®ä¸æ ï¼æ©å®¹è¦éæ°è®¡ç®ä¸æ ï¼å 为ä¸æ ç计ç®åæ°ç»é¿åº¦æå ³ï¼é¿åº¦æ¹åï¼ä¸æ ä¹åºå½éæ°è®¡ç®ã
å¨1.8åå ¶ä»¥ä¸çjdkçæ¬ä¸ï¼HashMapåå¼å ¥äºçº¢é»æ ã
红é»æ çå¼å ¥è¢«ç¨äºæ¿æ¢é¾è¡¨ï¼ä¸æ说å°ï¼å¦æå²çªè¿å¤ï¼ä¼å¯¼è´é¾è¡¨è¿é¿ï¼éä½æ¥è¯¢æ§è½ï¼ååçhashå½æ°è½ææçç¼è§£å²çªè¿å¤ï¼ä½æ¯å¹¶ä¸è½å®å ¨é¿å ãæ以HashMapå å ¥äºå¦ä¸ç§è§£å³æ¹æ¡ï¼å¨å¾é¾è¡¨å追å èç¹æ¶ï¼å¦æåç°é¾è¡¨é¿åº¦è¾¾å°8ï¼å°±ä¼å°é¾è¡¨è½¬ä¸ºçº¢é»æ ï¼ä»¥æ¤æåæ¥è¯¢çæ§è½ã
concurrenthashmap1.8源码如何详细解析?
ConcurrentHashMap在JDK1.8的线程安全机制基于CAS+synchronized实现,而非早期版本的分段锁。
在JDK1.7版本中,ConcurrentHashMap采用分段锁机制,包含一个Segment数组,每个Segment继承自ReentrantLock,并包含HashEntry数组,每个HashEntry相当于链表节点,用于存储key、value。默认支持个线程并发,每个Segment独立,互不影响。
对于put流程,与普通HashMap相似,首先定位至特定的Segment,然后使用ReentrantLock进行操作,后续过程与HashMap基本相同。
get流程简单,绝佳指标源码通过hash值定位至segment,再遍历链表找到对应元素。需要注意的是,value是volatile的,因此get操作无需加锁。
在JDK1.8版本中,线程安全的关键在于优化了put流程。首先计算hash值,遍历node数组。若位置为空,则通过CAS+自旋方式初始化。
若数组位置为空,尝试使用CAS自旋写入数据;若hash值为MOVED,表示需执行扩容操作;若满足上述条件均不成立,则使用synchronized块写入数据,同时判断链表或转换为红黑树进行插入。链表操作与HashMap相同,链表长度超过8时转换为红黑树。
get查询流程与HashMap基本一致,通过key计算位置,若table对应位置的key相同则返回结果;如为红黑树结构,则按照红黑树规则获取;否则遍历链表获取数据。
为什么HashMap是cs辅助源码线程不安全的
这是《Java程序员进阶之路》专栏的第篇,我们来聊聊为什么HashMap是线程不安全的。、多线程下扩容会死循环众所周知,HashMap是通过拉链法来解决哈希冲突的,也就是当哈希冲突时,会将相同哈希值的键值对通过链表的形式存放起来。
JDK7时,采用的是头部插入的方式来存放链表的,也就是下一个冲突的键值对会放在上一个键值对的前面(同一位置上的新元素被放在链表的头部)。扩容的时候就有可能导致出现环形链表,造成死循环。
resize方法的源码:
//newCapacity为新的容量voidresize(intnewCapacity){ //小数组,临时过度下Entry[]oldTable=table;//扩容前的容量intoldCapacity=oldTable.length;//MAXIMUM_CAPACITY为最大容量,2的次方=1<<if(oldCapacity==MAXIMUM_CAPACITY){ //容量调整为Integer的最大值0x7fffffff(十六进制)=2的次方-1threshold=Integer.MAX_VALUE;return;}//初始化一个新的数组(大容量)Entry[]newTable=newEntry[newCapacity];//把小数组的元素转移到大数组中transfer(newTable,initHashSeedAsNeeded(newCapacity));//引用新的大数组table=newTable;//重新计算阈值threshold=(int)Math.min(newCapacity*loadFactor,MAXIMUM_CAPACITY+1);}transfer方法用来转移,将小数组的元素拷贝到新的数组中。
voidtransfer(Entry[]newTable,booleanrehash){ //新的容量intnewCapacity=newTable.length;//遍历小数组for(Entry<K,V>e:table){ while(null!=e){ //拉链法,相同key上的不同值Entry<K,V>next=e.next;//是否需要重新计算hashif(rehash){ e.hash=null==e.key?0:hash(e.key);}//根据大数组的容量,和键的hash计算元素在数组中的下标inti=indexFor(e.hash,newCapacity);//同一位置上的新元素被放在链表的头部e.next=newTable[i];//放在新的数组上newTable[i]=e;//链表上的下一个元素e=next;}}}注意e.next=newTable[i]和newTable[i]=e这两行代码,就会将同一位置上的新元素被放在链表的头部。
扩容前的样子假如是下面这样子。
那么正常扩容后就是下面这样子。
假设现在有两个线程同时进行扩容,线程A在执行到newTable[i]=e;被挂起,android 摇杆源码此时线程A中:e=3、next=7、e.next=null
线程B开始执行,并且完成了数据转移。
此时,7的next为3,3的next为null。
随后线程A获得CPU时间片继续执行newTable[i]=e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:
执行下一轮循环,此时e=7,原本线程A中7的next为5,但由于table是线程A和线程B共享的,而线程B顺利执行完后,7的next变成了3,那么此时线程A中,7的next也为3了。
采用头部插入的方式,变成了下面这样子:
好像也没什么问题,此时next=3,e=3。
进行下一轮循环,但此时,由于线程B将3的next变为了null,所以此轮循环应该是最后一轮了。
接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互链接了,执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:
套娃开始,元素5也就成了弃婴,惨~~~
不过,JDK8时已经修复了这个问题,扩容时会保持链表原来的顺序,参照HashMap扩容机制的这一篇。
、多线程下put会导致元素丢失正常情况下,当发生哈希冲突时,HashMap是这样的:
但多线程同时执行put操作时,如果计算出来的索引位置是相同的,那会造成前一个key被后一个key覆盖,从而导致元素的丢失。
put的源码:
finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){ Node<K,V>[]tab;Node<K,V>p;intn,i;//步骤①:tab为空则创建if((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;//步骤②:计算index,并对null做处理if((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);else{ Node<K,V>e;Kk;//步骤③:节点key存在,直接覆盖valueif(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;//步骤④:判断该链为红黑树elseif(pinstanceofTreeNode)e=((TreeNode<K,V>)p).putTreeVal(this,tab,hash,key,value);//步骤⑤:该链为链表else{ for(intbinCount=0;;++binCount){ if((e=p.next)==null){ p.next=newNode(hash,key,value,null);//链表长度大于8转换为红黑树进行处理if(binCount>=TREEIFY_THRESHOLD-1)//-1for1sttreeifyBin(tab,hash);break;}//key已经存在直接覆盖valueif(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))break;p=e;}}//步骤⑥、直接覆盖if(e!=null){ //existingmappingforkeyVoldValue=e.value;if(!onlyIfAbsent||oldValue==null)e.value=value;afterNodeAccess(e);returnoldValue;}}++modCount;//步骤⑦:超过最大容量就扩容if(++size>threshold)resize();afterNodeInsertion(evict);returnnull;}问题发生在步骤②这里:
if((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);两个线程都执行了if语句,假设线程A先执行了tab[i]=newNode(hash,key,value,null),那table是这样的:
接着,线程B执行了tab[i]=newNode(hash,key,value,null),那table是这样的:
3被干掉了。
、put和get并发时会导致get到null线程A执行put时,因为元素个数超出阈值而出现扩容,线程B此时执行get,有可能导致这个问题。
注意来看resize源码:
finalNode<K,V>[]resize(){ Node<K,V>[]oldTab=table;intoldCap=(oldTab==null)?0:oldTab.length;intoldThr=threshold;intnewCap,newThr=0;if(oldCap>0){ //超过最大值就不再扩充了,就只好随你碰撞去吧if(oldCap>=MAXIMUM_CAPACITY){ threshold=Integer.MAX_VALUE;returnoldTab;}//没超过最大值,就扩充为原来的2倍elseif((newCap=oldCap<<1)<MAXIMUM_CAPACITY&&oldCap>=DEFAULT_INITIAL_CAPACITY)newThr=oldThr<<1;//doublethreshold}elseif(oldThr>0)//initialcapacitywasplacedinthresholdnewCap=oldThr;else{ //zeroinitialthresholdsignifiesusingdefaultsnewCap=DEFAULT_INITIAL_CAPACITY;newThr=(int)(DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY);}//计算新的resize上限if(newThr==0){ floatft=(float)newCap*loadFactor;newThr=(newCap<MAXIMUM_CAPACITY&&ft<(float)MAXIMUM_CAPACITY?(int)ft:Integer.MAX_VALUE);}threshold=newThr;@SuppressWarnings({ "rawtypes","unchecked"})Node<K,V>[]newTab=(Node<K,V>[])newNode[newCap];table=newTab;}线程A执行完table=newTab之后,线程B中的table此时也发生了变化,此时去get的时候当然会get到null了,因为元素还没有转移。
为了便于大家更系统化地学习Java,二哥已经将《Java程序员进阶之路》专栏开源到GitHub上了,大家只需轻轻地star一下,就可以和所有的小伙伴一起打怪升级了。
GitHub地址:/itwanger/toBeBetterJavaer
结合源码探究HashMap初始化容量问题
探究HashMap初始化容量问题
在深入研究HashMap源码时,有一个问题引人深思:为何在知道需要存储n个键值对时,我们通常会选择初始化容量为capacity = n / 0. + 1?
本文旨在解答这一疑惑,适合具备一定HashMap基础知识的读者。请在阅读前,思考以下问题:
让我们通过解答这些问题,逐步展开对HashMap初始化容量的深入探讨。
源码探究
让我们从实际代码出发,通过debug逐步解析HashMap的初始化逻辑。
举例:初始化一个容量为9的HashMap。
执行代码后,我们发现初始化容量为,且阈值threshold设置为。
解析
通过debug,我们首先关注到构造方法中的初始化逻辑。注意到,初始化阈值时,实际调用的是`tabliSizeFor(int n)`方法,它返回第一个大于等于n的2的幂。例如,`tabliSizeFor(9)`返回,`tabliSizeFor()`返回,`tabliSizeFor(8)`返回8。
继续解析
在构造方法结束后,我们通过debug继续追踪至`put`方法,直至`putVal`方法。
在`putVal`方法中,我们发现当第一次调用`put`时,table为null,从而触发初始化逻辑。在初始化过程中,关键在于`resize()`方法中对新容量`newCap`的初始化,即等于构造方法中设置的阈值`threshold`()。
阈值更新
在初始化后,我们进一步关注`updateNewThr`的代码逻辑,发现新的阈值被更新为新容量乘以负载因子,即 * 0.。
案例分析
举例:初始化一个容量为8的HashMap。
解答:答案是8,因为`tableSizeFor`方法返回大于等于参数的2的幂,而非严格大于。
扩容问题
举例:当初始化容量为时,放入9个不同的entry是否会引发扩容。
解答:不会,因为扩容条件与阈值有关,当map中存储的键值对数量大于阈值时才触发扩容。根据第一问,初始化容量是,阈值为 * 0. = 9,我们只放了9个,因此不会引起扩容。
容量选择
举例:已知需要存储个键值对,如何选择合适的初始化容量。
解答:初始化容量的目的是减少扩容次数以提高效率并节省空间。选择容量时,应考虑既能防止频繁扩容又能充分利用空间。具体选择取决于实际需求和预期键值对的数量。
总结
通过本文的探讨,我们深入了解了HashMap初始化容量背后的逻辑和原因。希望这些解析能够帮助您更深入地理解HashMap的内部工作原理。如果您对此有任何疑问或不同的见解,欢迎在评论区讨论。
最后,如有帮助,欢迎点赞分享。