jdk-HashMap-1.7-補充文章
此篇是關於初期的一篇HashMap文章的補充文章:主要涉及兩個東西,一、擴容;二、擴容時的執行緒安全分析。
在上述篇幅裡分析了hash過程,put過程和get過程。應該來說還是比較詳細的。
一、擴容
擴容應該是HashMap內一個非常常見的問題。此篇還是基於1.7去補充下,1.8的稍微複雜了一些是由於引入了紅黑樹進去。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
當put的時候addEntry方法記憶體在一個擴容的判斷:
1.當size>=threshold時(通俗的講就是當前個數是否大於閾值);
2.當前存在hash衝突了;
這裡需要重點分析的是第一種情況的一些特例,比如threshold,這個值的初始值來源於下面:
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
capacity初始預設值是16,loadFactory預設是0.75,也就是threshold預設是16*0.75=12。當個數大於12時,理論上就需要擴容了。
場景1:map中的陣列初始大小是16,那麼放進去的12個資料都放在了不同的陣列內(假設是0-11的位置上),這樣,當第13個放進來的時候(如果hash之後的位置是0-11(hash衝突了)),就需要擴容了。
場景2:map中的陣列初始大小是16,那麼放進去的12個資料都放在了不同的陣列內(假設是0-11的位置上),這樣,當第13個放進來的時候(如果hash之後的位置是12(hash沒有衝突)),那麼此時是不需要擴容的。這種情況下的極端例子就是16個資料在放置的時候都依次放在了16位的陣列中(0-15),這樣當17個資料來的時候才會擴容。
那麼在最初最多能存放多少資料而不發生擴容呢?
場景3:場景3更加極端一些,初始大小是16,閾值是12,那麼假設前11個值都落到了位置0上,也就是儲存到了陣列的同一個位置上,後續存入的15個資料都依次存放在1-15中(此時資料雖然大於閾值,但是沒有發生哈市衝突,所以不擴容),當第27個資料進來時,已經沒有位置了,必定發生衝突導致擴容。,所以最大的資料是11+15=26個資料。
擴容後續程式碼
resize:
1.擴容有最大值限定,2^30方。
2.transfer就是將原陣列的值放入新陣列中。
3.最後重新設定threshold(新的閾值)。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer: void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> 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;
}
}
}
transfer內沒什麼特殊的東西,就是重新計算hash的值在新陣列中的哪一個位置上。
這裡就引出了一個新的問題,關於transfer的執行緒安全問題,也可以說是HashMap的執行緒安全問題,大家都知道HashMap是執行緒不安全的,那麼提現在哪兒呢?一個就是put的時候,另一個就是擴容裡的transfer的時候。
put就不說了,比較容易理解。今天主要分析一下transfer的時候的執行緒安全問題;
基礎前提:
1.陣列初始大小為2
2.hash演算法取簡單的key%length 的大小。
單執行緒場景:
多執行緒場景:
多執行緒存在問題主要會是在哪裡呢?看單執行緒場景中,我們可以看見對於原陣列+連結串列的操作,存在兩個指標,一個e,一個e.next。這就是問題所在(對於連結串列的操作指標e,如果一個執行緒完整操作之後,後續執行緒再次操作時,連結串列的結構已經發生改變,那麼執行緒不安全也就無法避免)。
我們來看一看核心操作:
while(null != e) {
Entry<K,V> next = e.next; //1
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;
}
問題就出現在步驟1處,假設現在存在兩個執行緒A,執行緒B同時執行put操作。執行緒A執行到步驟1時,掛了。執行緒B正常執行。
因此執行緒A和執行緒B會出現下面的場景:
執行緒A再次被喚醒繼續執行擴容:
第一次迴圈,此時e指向key=3的節點,e.next指向key=7的節點。因此最終的結果就是執行緒A的位置3指向了key=3的處於執行緒B中的節點。
第二次迴圈,注意此時e和e.next的位置變化。這個時候e指向的是key=7,對於執行緒A來說當前存在指向key=3的資料,因此,key=7的next指向了key=3的節點,而key=7就變成了執行緒A的頭節點。
第三次迴圈,注意此時e又指向了key=3的節點,而e.next指向了null節點。如果針對key=3的節點再次操作的話,如下關鍵語句:
e.next = newTable[i];
key=3的next指向了第二次迴圈時的連結串列開頭資料key=7。所以就形成了一個環形結構,table[3]->key[3]->key[7]->[3]。這就是在多執行緒下可能出現的場景。相關文章
- Trace系列文章筆記目錄,陸續補充中...筆記
- JVM補充篇JVM
- linux命令補充Linux
- 聯通性補充
- Servlet學習補充Servlet
- css雜項補充CSS
- lambda(持續補充)
- while迴圈補充While
- 負載均衡補充負載
- explian type extra補充
- step1 補充
- redis筆記補充Redis筆記
- 博弈補充練習
- ReadFile 和 補充CreateFile
- ping(未完待補充)
- 陣列常用方法補充陣列
- 前端補充:url編碼前端
- 有關元件的補充~~~~~~~元件
- 網路流概念補充
- [Java併發]ThreadLocal補充Javathread
- 二分圖補充
- Jaeger知識點補充
- Spring註解補充(一)Spring
- [20180928]ora-01426(補充).txt
- 網路程式設計補充程式設計
- Office 365組命名策略 - 補充
- MybatisPlus的一些補充MyBatis
- Python補充02 Python小技巧Python
- 面試題抽答(補充)面試題
- 【排序】氣泡排序(待補充)排序
- Apollo 分散式配置中心(補充)分散式
- java 註解學習補充Java
- omnet6.0.1安裝補充
- CC1補充-LazyMap利用
- PS的一些補充
- Golang基礎語法補充Golang
- Scrapy 常用方法以及其補充
- CSS——浮動佈局(補充)CSS