jdk1.7中的HashMap
擴容部分源碼
擴容分為兩步
1.創建一個數組容量為原來2倍的HashMap
2.遍歷舊的Entry數組,把所有的Entry重新Hash到新數組
在開始之前我們先看一下擴容部分的源碼
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍歷舊數組中的每個桶
for (Entry<K,V> e : table) {
// 遍歷桶中的元素Entry(是一個鏈表)
while(null != e) {
Entry<K,V> next = e.next;
//如果是重新Hash,則需要重新計算hash值
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//計算該Entry在新table數組中對應桶的下標Index
/*
* indexFor中的代碼是return h & (length-1);
*可見hash對應的數組下標index是與hashmap的大小有關的,所以擴容不能直接把舊數據復制過來,需要遍歷重新計算下標index的值
*/
int i = indexFor(e.hash, newCapacity);
//頭插法
e.next = newTable[i];
newTable[i] = e;
//繼續下一個元素(next是再上面就存下來了的,是原來鏈表的下一個元素)
e = next;
}
}
}
擴容導致死循環
假設的前提條件:
- index求解為簡單的用key mod 數組的長度
- 最開始size=2 , key=3,7,5,則由假設1可知3 7 5 都在table[1]中
- 然后進行resize,使size變為4
未resize前的數據結構:
單線程
如果在單線程下根據上方transfer的源碼理解(遍歷+頭插法),最后擴容結果如下:
多線程
假設有兩個線程A,B都在進行擴容操作,但是線程A遍歷的時候在*處被暫時掛起了
1 void transfer(Entry[] newTable, boolean rehash) {
2 int newCapacity = newTable.length;
3 for (Entry<K,V> e : table) {
4 while(null != e) {
5 Entry<K,V> next = e.next;
6 if (rehash) {
7 e.hash = null == e.key ? 0 : hash(e.key);
8 }
9 int i = indexFor(e.hash, newCapacity);
10 e.next = newTable[i];
11 newTable[i] = e;--------* A被掛起
12 e = next;
13 }
14 }
15 }
①此時A線程e=3 | next=7(上方代碼第5行得)| i=3 (由假設1可知3%4=3) | e.next=null(10行代碼 3.next=null)| 在執行第11行的時候被線程掛起
此時A線程的結果
A線程被掛起,B線程正常執行,完全執行完resize擴容操作后,結果如下:
★由於B執行完了,所以現在java內存中newtable和table中的Entry都是B線程執行完的最新值★
★即: newTable[1]=5 | 5.next=null | newTable[3]=7 | 7.next=3 | 3.next=null★
此時再切換回A線程,A線程重新繼續執行11行的代碼,①已經求得的臨時變量的值是沒變的。
上面①中的臨時變量繼續使用
11 newTable[i] = e; -----(i=3,e=3帶入得newTable[3]=3)
12 e = next; -----(next=7,帶入得e=7)
執行結果如下:
繼續循環(此時e=7 由上次循環的代碼可知):
5 next = e.next; -----(由上圖可知7.next=3故得next=3)
9 i = indexFor(e.hash,newCapacity); -----(由假設1可知index=7%4=3)
10 e.next = newTable[i]; -----(由上圖可知newTable[3]=3故得e.next=3)
11 newTable[i] = e; -----(newTable[3]=7)
12 e = next -----(e=3)
結果如下:
繼續循環(此時e=3 由上次循環的代碼可知):
5 next = e.next; -----(由上圖可知3.next=null故得next=null)
9 i = indexFor(e.hash,newCapacity); -----(由假設1可知index=3%4=3)
10 e.next = newTable[i]; -----(由上圖可知newTable[3]=7故得e.next=7)
11 newTable[i] = e; -----(newTable[3]=3)
12 e = next -----(e=null)
至此e=null,循環結束
結果如下:
可見這出現了一個循環鏈表,在之后只要涉及輪詢hashmap數據結構,就會在這里發生死循環。
擴容導致數據丟失
假設前提條件
- index求解為簡單的用key mod 數組的長度
- 最開始size=2 , key=7,5,3,則由假設1可知7 5 3 都在table[1]中
- 然后進行resize,使size變為4
同樣兩個線程A,B都在進行擴容,A在*處被掛起
1 void transfer(Entry[] newTable, boolean rehash) {
2 int newCapacity = newTable.length;
3 for (Entry<K,V> e : table) {
4 while(null != e) {
5 Entry<K,V> next = e.next;
6 if (rehash) {
7 e.hash = null == e.key ? 0 : hash(e.key);
8 }
9 int i = indexFor(e.hash, newCapacity);
10 e.next = newTable[i];
11 newTable[i] = e;--------* A被掛起
12 e = next;
13 }
14 }
15 }
①此時A線程e=7 | next=5(上方代碼第5行得)| i=3 (由假設1可知7%4=3) | e.next=null(10行代碼 3.next=null)| 在執行第11行的時候被線程掛起
執行結果如下:
A線程被掛起,B線程正常執行,完全執行完resize擴容操作后,結果如下:
★由於B執行完了,所以現在java內存中newtable和table中的Entry都是B線程執行完的最新值★
★即: newTable[1]=5 | 5.next=null | newTable[3]=3 | 3.next=7 | 7.next=null★
此時再切換回A線程,A線程重新繼續執行11行的代碼,①已經求得的臨時變量的值是沒變的。
上面①中的臨時變量繼續使用:①此時A線程e=7 | next=5(上方代碼第5行得)| i=3 (由假設1可知3%4=3) | e.next=null(10行代碼 7.next=null)| 在執行第11行的時候被線程掛起
11 newTable[i] = e; -----(i=3,e=7帶入得newTable[3]=7)
12 e = next; -----(next=5,帶入得e=5)
執行結果如下:
繼續循環(此時e=5 由上次循環的代碼可知):
5 next = e.next; -----(由上圖可知5.next=null故得next=null)
9 i = indexFor(e.hash,newCapacity); -----(由假設1可知index=5%4=1)
10 e.next = newTable[i]; -----(由上圖可知newTable[1]=5故得e.next=5)
11 newTable[i] = e; -----(newTable[1]=5)
12 e = next -----(e=null)
至此e=null,循環結束
結果如下:
3元素丟失,並形成環形鏈表。並在后續操作hashmap時造成死循環。
jdk1.8中的HashMap
jdk1.8中對HashMap進行了優化,如果發生hash碰撞,不采用頭插法,改成了尾插法,因此不會出現循環鏈表的情況。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果插入的位置為null,沒有hash碰撞,則直接插入元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
.........
但是在多線程的環境下仍然不安全,當兩個線程A,B同時進行Put的操作的時候,都判斷當前沒有hash碰撞,然后同時進行直接插入,那么后面哪個線程的值Entry會把前一個線程插入的Entry給**覆蓋**掉,發生線程不安全