1.HashMap为ä»ä¹ä¸å®å
¨ï¼
2.为什么HashMap是源码线程不安全的
HashMap为ä»ä¹ä¸å®å ¨ï¼
æ们é½ç¥éHashMapæ¯çº¿ç¨ä¸å®å ¨çï¼å¨å¤çº¿ç¨ç¯å¢ä¸ä¸å»ºè®®ä½¿ç¨ï¼ä½æ¯å ¶çº¿ç¨ä¸å®å ¨ä¸»è¦ä½ç°å¨ä»ä¹å°æ¹å¢ï¼æ¬æå°å¯¹è¯¥é®é¢è¿è¡è§£å¯ã1.jdk1.7ä¸çHashMap
å¨jdk1.8ä¸å¯¹HashMapåäºå¾å¤ä¼åï¼è¿éå åæå¨jdk1.7ä¸çé®é¢ï¼ç¸ä¿¡å¤§å®¶é½ç¥éå¨jdk1.7å¤çº¿ç¨ç¯å¢ä¸HashMap容æåºç°æ»å¾ªç¯ï¼è¿éæ们å ç¨ä»£ç æ¥æ¨¡æåºç°æ»å¾ªç¯çæ åµï¼
public class HashMapTest { public static void main(String[] args) { HashMapThread thread0 = new HashMapThread(); HashMapThread thread1 = new HashMapThread(); HashMapThread thread2 = new HashMapThread(); HashMapThread thread3 = new HashMapThread(); HashMapThread thread4 = new HashMapThread(); thread0.start(); thread1.start(); thread2.start(); thread3.start(); thread4.start(); }}class HashMapThread extends Thread { private static AtomicInteger ai = new AtomicInteger(); private static Map map = new HashMap<>(); @Override public void run() { while (ai.get() < ) { map.put(ai.get(), ai.get()); ai.incrementAndGet(); } }}
ä¸è¿°ä»£ç æ¯è¾ç®åï¼å°±æ¯å¼å¤ä¸ªçº¿ç¨ä¸æè¿è¡putæä½ï¼å¹¶ä¸HashMapä¸AtomicIntegeré½æ¯å ¨å±å ±äº«çã
å¨å¤è¿è¡å 次该代ç åï¼åºç°å¦ä¸æ»å¾ªç¯æ å½¢ï¼
å ¶ä¸æå 次è¿ä¼åºç°æ°ç»è¶ççæ åµï¼
è¿éæ们çéåæ为ä»ä¹ä¼åºç°æ»å¾ªç¯çæ åµï¼éè¿jpsåjstackå½åæ¥çæ»å¾ªç¯æ åµï¼ç»æå¦ä¸ï¼
ä»å æ ä¿¡æ¯ä¸å¯ä»¥çå°åºç°æ»å¾ªç¯çä½ç½®ï¼éè¿è¯¥ä¿¡æ¯å¯æç¡®ç¥éæ»å¾ªç¯åçå¨HashMapçæ©å®¹å½æ°ä¸ï¼æ ¹æºå¨transferå½æ°ä¸ï¼jdk1.7ä¸HashMapçtransferå½æ°å¦ä¸ï¼
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry e : table) { while(null != e) { Entry next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
æ»ç»ä¸è¯¥å½æ°ç主è¦ä½ç¨ï¼
å¨å¯¹tableè¿è¡æ©å®¹å°newTableåï¼éè¦å°åæ¥æ°æ®è½¬ç§»å°newTableä¸ï¼æ³¨æ-è¡ä»£ç ï¼è¿éå¯ä»¥çåºå¨è½¬ç§»å ç´ çè¿ç¨ä¸ï¼ä½¿ç¨çæ¯å¤´ææ³ï¼ä¹å°±æ¯é¾è¡¨ç顺åºä¼ç¿»è½¬ï¼è¿éä¹æ¯å½¢ææ»å¾ªç¯çå ³é®ç¹ã
ä¸é¢è¿è¡è¯¦ç»åæã
1.1 æ©å®¹é ææ»å¾ªç¯åæè¿ç¨
åææ¡ä»¶ï¼è¿éå设ï¼
hashç®æ³ä¸ºç®åçç¨key modé¾è¡¨ç大å°ã
æå¼å§hash表size=2ï¼key=3,7,5ï¼åé½å¨table[1]ä¸ã
ç¶åè¿è¡resizeï¼ä½¿sizeåæ4ã
æªresizeåçæ°æ®ç»æå¦ä¸ï¼
请ç¹å»è¾å ¥å¾çæè¿°
å¦æå¨å线ç¨ç¯å¢ä¸ï¼æåçç»æå¦ä¸ï¼
请ç¹å»è¾å ¥å¾çæè¿°
è¿éç转移è¿ç¨ï¼ä¸åè¿è¡è¯¦è¿°ï¼åªè¦ç解transferå½æ°å¨åä»ä¹ï¼å ¶è½¬ç§»è¿ç¨ä»¥åå¦ä½å¯¹é¾è¡¨è¿è¡å转åºè¯¥ä¸é¾ã
ç¶åå¨å¤çº¿ç¨ç¯å¢ä¸ï¼å设æ两个线ç¨AåBé½å¨è¿è¡putæä½ã线ç¨Aå¨æ§è¡å°transferå½æ°ä¸ç¬¬è¡ä»£ç å¤æèµ·ï¼å 为该å½æ°å¨è¿éåæçå°ä½é常éè¦ï¼å æ¤å次贴åºæ¥ã
请ç¹å»è¾å ¥å¾çæè¿°
æ¤æ¶çº¿ç¨Aä¸è¿è¡ç»æå¦ä¸ï¼
请ç¹å»è¾å ¥å¾çæè¿°
线ç¨Aæèµ·åï¼æ¤æ¶çº¿ç¨Bæ£å¸¸æ§è¡ï¼å¹¶å®æresizeæä½ï¼ç»æå¦ä¸ï¼
请ç¹å»è¾å ¥å¾çæè¿°
è¿ééè¦ç¹å«æ³¨æçç¹ï¼ç±äºçº¿ç¨Bå·²ç»æ§è¡å®æ¯ï¼æ ¹æ®Javaå å模åï¼ç°å¨newTableåtableä¸çEntryé½æ¯ä¸»åä¸ææ°å¼ï¼7.next=3ï¼3.next=nullã
æ¤æ¶åæ¢å°çº¿ç¨Aä¸ï¼å¨çº¿ç¨Aæèµ·æ¶å åä¸å¼å¦ä¸ï¼e=3ï¼next=7ï¼newTable[3]=nullï¼ä»£ç æ§è¡è¿ç¨å¦ä¸ï¼
newTable[3]=e ----> newTable[3]=3e=next ----> e=7 æ¤æ¶ç»æå¦ä¸ï¼è¯·ç¹å»è¾å ¥å¾çæè¿°
继ç»å¾ªç¯ï¼
e=7next=e.next ----> next=3ãä»ä¸»åä¸åå¼ãe.next=newTable[3] ----> e.next=3ãä»ä¸»åä¸åå¼ãnewTable[3]=e ----> newTable[3]=7e=next ----> e=3 ç»æå¦ä¸ï¼è¯·ç¹å»è¾å ¥å¾çæè¿°
å次è¿è¡å¾ªç¯ï¼
e=3next=e.next ----> next=nulle.next=newTable[3] ----> e.next=7 å³ï¼3.next=7newTable[3]=e ----> newTable[3]=3e=next ----> e=null 注ææ¤æ¬¡å¾ªç¯ï¼e.next=7ï¼èå¨ä¸æ¬¡å¾ªç¯ä¸7.next=3ï¼åºç°ç¯å½¢é¾è¡¨ï¼å¹¶ä¸æ¤æ¶e=null循ç¯ç»æãç»æå¦ä¸ï¼
请ç¹å»è¾å ¥å¾çæè¿°
å¨åç»æä½ä¸åªè¦æ¶å轮询hashmapçæ°æ®ç»æï¼å°±ä¼å¨è¿éåçæ»å¾ªç¯ï¼é ææ²å§ã
1.2 æ©å®¹é ææ°æ®ä¸¢å¤±åæè¿ç¨
éµç §ä¸è¿°åæè¿ç¨ï¼åå§æ¶ï¼
请ç¹å»è¾å ¥å¾çæè¿°
线ç¨Aå线ç¨Bè¿è¡putæä½ï¼åæ ·çº¿ç¨Aæèµ·ï¼
请ç¹å»è¾å ¥å¾çæè¿°
æ¤æ¶çº¿ç¨Açè¿è¡ç»æå¦ä¸ï¼
请ç¹å»è¾å ¥å¾çæè¿°
æ¤æ¶çº¿ç¨Bå·²è·å¾CPUæ¶é´çï¼å¹¶å®æresizeæä½ï¼
请ç¹å»è¾å ¥å¾çæè¿°
åæ ·æ³¨æç±äºçº¿ç¨Bæ§è¡å®æï¼newTableåtableé½ä¸ºææ°å¼ï¼5.next=nullã
æ¤æ¶åæ¢å°çº¿ç¨Aï¼å¨çº¿ç¨Aæèµ·æ¶ï¼e=7ï¼next=5ï¼newTable[3]=nullã
æ§è¡newtable[i]=eï¼å°±å°7æ¾å¨äºtable[3]çä½ç½®ï¼æ¤æ¶next=5ãæ¥çè¿è¡ä¸ä¸æ¬¡å¾ªç¯ï¼
e=5next=e.next ----> next=nullï¼ä»ä¸»åä¸åå¼e.next=newTable[1] ----> e.next=5ï¼ä»ä¸»åä¸åå¼newTable[1]=e ----> newTable[1]=5e=next ----> e=null å°5æ¾ç½®å¨table[1]ä½ç½®ï¼æ¤æ¶e=null循ç¯ç»æï¼3å ç´ ä¸¢å¤±ï¼å¹¶å½¢æç¯å½¢é¾è¡¨ã并å¨åç»æä½hashmapæ¶é ææ»å¾ªç¯ã请ç¹å»è¾å ¥å¾çæè¿°
2.jdk1.8ä¸HashMap
å¨jdk1.8ä¸å¯¹HashMapè¿è¡äºä¼åï¼å¨åçhash碰æï¼ä¸åéç¨å¤´ææ³æ¹å¼ï¼èæ¯ç´æ¥æå ¥é¾è¡¨å°¾é¨ï¼å æ¤ä¸ä¼åºç°ç¯å½¢é¾è¡¨çæ åµï¼ä½æ¯å¨å¤çº¿ç¨çæ åµä¸ä»ç¶ä¸å®å ¨ï¼è¿éæ们çjdk1.8ä¸HashMapçputæä½æºç ï¼
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) // å¦æ没æhash碰æåç´æ¥æå ¥å ç´ tab[i] = newNode(hash, key, value, null); else { Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } è¿æ¯jdk1.8ä¸HashMapä¸putæä½ç主å½æ°ï¼ 注æ第6è¡ä»£ç ï¼å¦æ没æhash碰æåä¼ç´æ¥æå ¥å ç´ ãå¦æ线ç¨Aå线ç¨Båæ¶è¿è¡putæä½ï¼å好è¿ä¸¤æ¡ä¸åçæ°æ®hashå¼ä¸æ ·ï¼å¹¶ä¸è¯¥ä½ç½®æ°æ®ä¸ºnullï¼æ以è¿çº¿ç¨AãBé½ä¼è¿å ¥ç¬¬6è¡ä»£ç ä¸ã
å设ä¸ç§æ åµï¼çº¿ç¨Aè¿å ¥åè¿æªè¿è¡æ°æ®æå ¥æ¶æèµ·ï¼è线ç¨Bæ£å¸¸æ§è¡ï¼ä»èæ£å¸¸æå ¥æ°æ®ï¼ç¶å线ç¨Aè·åCPUæ¶é´çï¼æ¤æ¶çº¿ç¨Aä¸ç¨åè¿è¡hashå¤æäºï¼é®é¢åºç°ï¼çº¿ç¨Aä¼æ线ç¨Bæå ¥çæ°æ®ç»è¦çï¼åç线ç¨ä¸å®å ¨ã
æ»ç»
é¦å HashMapæ¯çº¿ç¨ä¸å®å ¨çï¼å ¶ä¸»è¦ä½ç°ï¼
å¨jdk1.7ä¸ï¼å¨å¤çº¿ç¨ç¯å¢ä¸ï¼æ©å®¹æ¶ä¼é æç¯å½¢é¾ææ°æ®ä¸¢å¤±ã
å¨jdk1.8ä¸ï¼å¨å¤çº¿ç¨ç¯å¢ä¸ï¼ä¼åçæ°æ®è¦ççæ åµã
为什么HashMap是线程不安全的
这是《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;被挂起,此时线程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。
进行下一轮循环,78源码16位但此时,由于线程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