探究HashMap線性不安全(一)——重溫HashMap的put操作


內容

​  網上很多資料都詳細地講解了HashMap底層的實現,但是講到HashMap的並發操作不是線性安全時,往往一筆帶過:在多個線程並發擴容時,會在執行transfer()方法轉移鍵值對時,造成鏈表成環,導致程序在執行get操作時形成死循環

​  對於沒有研究過該過程的童鞋,很難費解這句話的含義。下面筆者分四個小節帶着大家共同研究一下JDK1.7和JDK1.8版本下HashMap的線性不安全是怎么發生的,並詳細探究鏈表成環的形成過程。如果你對於HashMap底層的put、get操作不清楚,建議先學習參考1中的內容。

適合人群

  ​Java進階

說明

  轉載請說明出處:探究HashMap線性不安全(一)——重溫HashMap的put操作

參考

​   1、https://www.toutiao.com/i6544826418210013700/ HashMap底層數據結構原理

​   2、https://www.toutiao.com/i6545790064104833539/ 為什么HashMap非線程安全

​   3、https://blog.csdn.net/qq_32182461/article/details/81152025 hashmap並發情況下的成環原因(筆者認為該文是一種誤解)

正文

​  了解過HashMap底層實現的童鞋都知道,向HashMap存入鍵值對時,如果當前map中鍵值對的個數size已經大於等於擴容的閾值threshold,並且對應鏈表上數據不為空時,線程會執行resize()方法對HashMap擴容。過程如下:

 1 public V put(K key, V value) {  2     //判斷key是否為null,如果是null則將該鍵值對存放到到index為0的位置上
 3     if (key == null)  4         return putForNullKey(value);  5     //計算key的hash值
 6     int hash = hash(key);  7     //對hash值取模求key對應的index
 8     int i = indexFor(hash, table.length);  9     //判斷key是否已經存在,若存在則覆蓋對應的value值,並返回舊value值
10     for (Entry<K,V> e = table[i]; e != null; e = e.next) { 11  Object k; 12         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 13             V oldValue = e.value; 14             e.value = value; 15             e.recordAccess(this); 16             return oldValue; 17  } 18  } 19     
20     modCount++; 21     //若鍵值對不存在,則插入到table中
22  addEntry(hash, key, value, i); 23     return null; 24 } 
 1 void addEntry(int hash, K key, V value, int bucketIndex) { 2     //擴容的兩個條件:map中鍵值對的個數size已經大於等於擴容的閾值threshold,table的當前位置上已經存在鍵值對。
 3     if ((size >= threshold) && (null != table[bucketIndex])) { 4         //擴容操作具體由resize()方法執行。
 5         resize(2 * table.length); 6         hash = (null != key) ? hash(key) : 0; 7         bucketIndex = indexFor(hash, table.length); 8 } 9      //將鍵值對存入到指定index位置的鏈上
10 createEntry(hash, key, value, bucketIndex); 11 }

   resize()方法中的transfer()方法用於將oldTable中的原有鍵值對信息復制到擴容后的newTable中。

 1 void resize(int newCapacity) {  2     //使用oldTable指向擴容前的table
 3     Entry[] oldTable = table;  4     int oldCapacity = oldTable.length;  5     //如果hashMap的容量已經達到最大值,那么將擴容閾值threshold設置為Integer的最大值
 6     if (oldCapacity == MAXIMUM_CAPACITY) {  7         threshold = Integer.MAX_VALUE;  8         return;  9  } 10     //按照傳入的容量,創建新的table
11     Entry[] newTable = new Entry[newCapacity]; 12     //useAltHashing在初始化后為false
13     boolean oldAltHashing = useAltHashing; 14     useAltHashing |= sun.misc.VM.isBooted() &&
15             (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 16     //JVM啟動后,但由於擴容后的容量newCapacity<ALTERNATIVE_HASHING_THRESHOLD,useAltHashing也為false 17     //false與false異或,rehash=false,因此不會對key值重新進行hash計算。
18     boolean rehash = oldAltHashing ^ useAltHashing; 19     //進行新舊table數據的遷移
20  transfer(newTable, rehash); 21     //將table指向遷移后的newTable
22     table = newTable; 23     //按照計算公式為newCapacity * loadFactor更新擴容閾值threshold
24     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); 25 }

 ​  通過調試可知Holder.ALTERNATIVE_HASHING_THRESHOLD為Integer.MAX_VALUE

1538202865608_thumb3

​  因此默認情況下rehash為false,擴容過程中不會對key值重新計算hash。下一節將詳細探究HashMap擴容的鍵值對遷移過程,多線程並發執行transfer()方法是如何產生環形鏈表的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM