【集合之HashMap】HashMap实现原理及非线程安全原因
要知道HashMap是什么,首先要搞清楚它的數據結構,在Java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個數組和鏈表的結合體(在數據結構中,一般稱之為“鏈表散列“)。
從圖中我們可以看到一個HashMap就是一個數組結構,當新建一個HashMap的時候,就會初始化一個數組。
?
HashMap源碼中的3個常量,
1 /** 2 * The default initial capacity - MUST be a power of two. 3 */ 4 static final int DEFAULT_INITIAL_CAPACITY = 16; 5 6 /** 7 * The maximum capacity, used if a higher value is implicitly specified 8 * by either of the constructors with arguments. 9 * MUST be a power of two <= 1<<30. 10 */ 11 static final int MAXIMUM_CAPACITY = 1 << 30; 12 13 /** 14 * The load factor used when none specified in constructor. 15 */ 16 static final float DEFAULT_LOAD_FACTOR = 0.75f; View Code這三個常量其實是對應HashMap的三個性能參數的,這三個參數的意義如下:
1、DEFAULT_INITIAL_CAPACITY初始值為16。該初始值可以外部傳參數指定,必須為2的整數次冪。如果外部指定的容量參數大于MAXIMUM_CAPACITY,則會按MAXIMUM_CAPACITY來處理,否則會計算一個大于該參數并最接近該參數的2的整數次冪最為HashMap的初始容量。
2、MAXIMUM_CAPACITY是HashMap中元素容量的最大值(1<<30)。
3、DEFAULT_LOAD_FACTOR是默認的hash因子(0.75f),可以外部指定,必須小于1.0f,否則不能rehash。
?
HashMap的put操作,
1 public V put(K key, V value) { 2 if (key == null) 3 return putForNullKey(value); 4 int hash = hash(key.hashCode()); 5 int i = indexFor(hash, table.length); 6 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 7 Object k; 8 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 9 V oldValue = e.value; 10 e.value = value; 11 e.recordAccess(this); 12 return oldValue; 13 } 14 } 15 16 modCount++; 17 addEntry(hash, key, value, i); 18 return null; 19 } 20 21 static int hash(int h) { 22 // This function ensures that hashCodes that differ only by 23 // constant multiples at each bit position have a bounded 24 // number of collisions (approximately 8 at default load factor). 25 h ^= (h >>> 20) ^ (h >>> 12); 26 return h ^ (h >>> 7) ^ (h >>> 4); 27 } 28 29 static int indexFor(int h, int length) { 30 return h & (length-1); 31 } 32 33 void addEntry(int hash, K key, V value, int bucketIndex) { 34 Entry<K,V> e = table[bucketIndex]; 35 table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 36 if (size++ >= threshold) 37 resize(2 * table.length); 38 } 39 40 void resize(int newCapacity) { 41 Entry[] oldTable = table; 42 int oldCapacity = oldTable.length; 43 if (oldCapacity == MAXIMUM_CAPACITY) { 44 threshold = Integer.MAX_VALUE; 45 return; 46 } 47 48 Entry[] newTable = new Entry[newCapacity]; 49 transfer(newTable); 50 table = newTable; 51 threshold = (int)(newCapacity * loadFactor); 52 } 53 54 void transfer(Entry[] newTable) { 55 Entry[] src = table; 56 int newCapacity = newTable.length; 57 for (int j = 0; j < src.length; j++) { 58 Entry<K,V> e = src[j]; 59 if (e != null) { 60 src[j] = null; 61 do { 62 Entry<K,V> next = e.next; 63 int i = indexFor(e.hash, newCapacity); 64 e.next = newTable[i]; 65 newTable[i] = e; 66 e = next; 67 } while (e != null); 68 } 69 } 70 } View Code從上面這段代碼可以看出HashMap的put操作具體處理過程如下:
1、取出key的hashcode,調用hash函數來將hashcode高地位異或成一個新的值,這個hash函數是數學上的擾動函數,可以達到比較好的解決hash沖突的效果。
2、indexFor方法中將h和length-1做與運算,由于length是2的整數次冪,所以這個函數相當于取模length。
3、根據2中返回的數組位置去遍歷該位置對應的鏈表是否存在該元素,存在的話就用新的value替換舊的value,并返回舊的value,跳出put操作。否則就進行步驟4。
4、modeCount用來記錄對HashMap修改的,做一次修改加一,相當于版本號,用于迭代器遍歷的過程中同時有人修改了HashMap時拋異常,所以HashMap在迭代器遍歷的時候是不允許其他線程修改的。完成了3以后則將進入addEntry方法來將新元素掛載數組對應的鏈表頭部。如果HashMap當前的元素個數不小于capacity * loadFactor,還需要進行rehash(即重新對HashMap做翻天覆地的調整)。
5、rehash的過程其實是將舊的散列鏈表中的元素全部放到二倍大小的新的散列鏈表中,transfer方法會將每條鏈表從頭至尾遍歷后放入新的位置,放置到新鏈表的過程會逆原來順序。
?
HashMap的get、remove相對比較簡單就是遍歷散列鏈表的過程,putAll操作是首先用參數中的map的size對當前HashMap做一次rehash判斷。
?
HashMap的死循環,主要發生在多線程rehash的過程中,又有線程去get的時候,分析過程如下:
單線程的rehash過程:
?
多線程的rehash過程:
1、假設我們有兩個線程。我用紅色和淺藍色標注了一下。
我們再回頭看一下我們的 transfer代碼中的這個細節:
1 do { 2 Entry<K,V> next = e.next; // 假設線程一執行到這里就被調度掛起了 3 int i = indexFor(e.hash, newCapacity); 4 e.next = newTable[i]; 5 newTable[i] = e; 6 e = next; 7 } while (e != null); View Code而我們的線程二執行完成了。于是我們有下面的這個樣子。
注意,因為Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。我們可以看到鏈表的順序被反轉了。
2、線程一被調度回來執行。
先是執行 newTalbe[i] = e;
然后是e = next,導致了e指向了key(7),
而下一次循環的next = e.next導致了next指向了key(3),
?
3、一切安好。
線程一接著工作。把key(7)摘下來,放到newTable[i]的第一個,然后把e和next往下移。
?
4、環形鏈接出現。
e.next = newTable[i] 導致 ?key(3).next 指向了 key(7)
注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。
?
于是,當我們的線程一調用到第4個數組元素對應的鏈表時就死循環了。
轉載于:https://www.cnblogs.com/youxuanhu/p/5790798.html
總結
以上是生活随笔為你收集整理的【集合之HashMap】HashMap实现原理及非线程安全原因的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 夺命雷公狗—angularjs—19—a
- 下一篇: BZOJ3270: 博物馆