JDk1.7 HashMap原始碼解析——執行緒安全問題
Jdk1.7的HashMap, 在多執行緒環境下,擴容的時候可能會形成環狀連結串列導致死迴圈和資料丟失問題。
HashMap在擴容的流程
- 擴容相關常量
-
DEFAULT_LOAD_FACTOR
: 預設負載因子,這個引數是判斷擴容時的重要引數,當Map中的元素的數量達到最大容量乘上負載因子時,就會進行擴容。如果在構造方法中沒有指定,那麼預設就是0.75。這個0.75是個非常合理的值,如果負載因子等於1,那麼只有元素數量達到最大容量的時候才會進行擴容,導致每一個桶的連結串列長度都過長,執行效率變低。如果負載因子等於0.5,那麼Map每儲存一半的元素就擴容,浪費記憶體空間。 -
threshold
: 容量達到閾值時(threshold = 初始容量 * 載入因子),在 put 資料時,就會擴容,相當於實際使用的容量。 -
table
:儲存Entry也就是我們儲存的key,value的物件陣列,擴容時會 new 一個新的陣列,長度為老陣列的一倍,然後逐一將這個table的元素移至新的陣列,然後將新的陣列覆蓋原陣列來實現擴容。
/**
`* 預設負載因子
`*/`
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
`* Entry陣列
`*/`
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
* 容量, 預設達到 size * 0.75 就會擴容
*/
transient int size;
- 擴容的條件
當我們新增元素(put 方法)的時候,需要去判斷是否能夠存下這個元素,如果存的下就存,存不下就擴容再存。
- 擴容流程
以 put 方法 為例
public V put(K key, V value) {
// 判斷是否是空表
if (table == EMPTY_TABLE) {
// 初始化, 強制把 初始化容量 轉換為 2 的整次冪
inflateTable(threshold);
}
// 判斷是否是空值
if (key == null)
return putForNullKey(value);
// 獲取 hash 值
int hash = hash(key);
// 獲取 索引
int i = indexFor(hash, table.length);
// 遍歷 指定索引下的連結串列
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 判斷put 的key是否已經存在
// 如果存在,則替換
// 並返回 舊值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 如果 put 的key不存在,則新增進去
// 此方法會判斷是否要擴容
addEntry(hash, key, value, i);
return null;
}
addEntry
方法, 會判斷陣列當前容量是否已經超過的閾值,例如,假設當前的陣列容量是16,載入因子為0.75,即超過了12,並且剛好要插入的索引處有元素,這時候就需要進行擴容操作,可以看到resize
擴容大小是原陣列的兩倍,仍然符合陣列的長度是2的指數次冪。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 判斷是否需要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容
resize(2 * table.length);
// 重新計算hash值
hash = (null != key) ? hash(key) : 0;
// 計算所要插入的桶的索引值
bucketIndex = indexFor(hash, table.length);
}
// 新增Entry
createEntry(hash, key, value, bucketIndex);
}
resize
方法, 首先,如果這個HashMap的容量已經非常大了,新的長度會大於我們預設的最大容量,這時直接return;來終止這個方法。如果沒有,後面會進行陣列的轉移操作,即transfer
方法。
initHashSeedAsNeeded
方法, 主要是判斷一下是否需要初始化雜湊,儘量避免HashMap的值太過集中不夠雜湊。
這裡 預設的最大容量是 MAXIMUM_CAPACITY = 1 << 30
, 就是 MAXIMUM_CAPACITY = 2 ^ 30
, 如果達到最大 預設容量, 那麼,HashMap 可以使用的 的容量就是 Integer.MAX_VALUE
。
/**
* 擴容
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 達到最大值,無法擴容
if (oldCapacity == MAXIMUM_CAPACITY) {
// 設定為 HashMap 的最大容量
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
// 將資料轉移到新的Entry[]陣列中
// 新陣列是舊陣列的兩倍大小
transfer(newTable, initHashSeedAsNeeded(newCapacity));//初始化雜湊種子
// 覆蓋原陣列
table = newTable;
// 重新計算 可以使用的容量
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer
方法, 把原來陣列的值逐一複製到這個新的陣列, 首先是遍歷table陣列,如果遍歷到Entry不為空,我們進入while迴圈,進行連結串列操作,每次操作結束都將進入迴圈的e用e.next覆蓋,直至連結串列到達尾部。
/**
*
* @param newTable 新陣列的引用
* @param rehash true代表需要重新獲取 hash 值
*/
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;
}
}
}
多執行緒下,連結串列的插入
這裡 模擬了兩個執行緒 A 和 B,在併發情況下,插入資料的擴容圖示:
注意: 圖中所有的箭頭都是指標(或者引用),執行緒 A 的新陣列 是 陣列A;執行緒 B 的新陣列 是 陣列B。
假設,執行緒 A 執行到
Entry<K,V> next = e.next;
的程式碼時阻塞了, 因為執行緒 A 被阻塞了,其後面的程式碼就沒法繼續執行了,而此時執行緒 B 也進入方法進行擴容,擴容後的結果就是單執行緒時擴容後的結果,此時相比於擴容前的HashMap,原陣列的連結串列元素的位置已經調換。
執行緒 B 的圖示 是就是簡單的單執行緒擴容,就只畫出執行後的結果圖;
執行緒 A 的圖示 依賴於 執行緒 B 的結果,每個圖示,代表一次 while 迴圈後的結果。
圖示:
可以看到,最後出現了環,並且 資料 8 丟失了。
相關文章
- 深入解讀HashMap執行緒安全性問題HashMap執行緒
- HashMap多執行緒併發問題分析HashMap執行緒
- 03 執行緒安全問題執行緒
- SimpleDateFormat 執行緒安全問題ORM執行緒
- HashMap jdk1.7和1.8原始碼剖析HashMapJDK原始碼
- Jdk1.7下的HashMap原始碼分析JDKHashMap原始碼
- 執行緒池執行模型原始碼全解析執行緒模型原始碼
- ArrayList 的執行緒安全問題執行緒
- 深入JAVA執行緒安全問題Java執行緒
- 多執行緒,你覺得你安全了?(執行緒安全問題)執行緒
- Java原始碼解析 ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java原始碼解析 - ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java執行緒池ThreadPoolExecutor原始碼解析Java執行緒thread原始碼
- Java多執行緒中執行緒安全與鎖問題Java執行緒
- HashMap為何執行緒不安全HashMap執行緒
- 從原始碼的角度解析執行緒池執行原理原始碼執行緒
- Netty原始碼解析一——執行緒池模型之執行緒池NioEventLoopGroupNetty原始碼執行緒模型OOP
- parallelStream中的執行緒安全問題Parallel執行緒
- ConcurrentHashMap原始碼解析,多執行緒擴容HashMap原始碼執行緒
- 執行緒安全使用 HashMap 的四種技巧執行緒HashMap
- HashMap為何執行緒不安全?HashMap,HashTable,ConcurrentHashMap對比HashMap執行緒
- 模板方法中的執行緒安全問題執行緒
- lambda中stream執行緒安全的問題執行緒
- 從FMDB執行緒安全問題說起執行緒
- 單例模式執行緒安全reorder問題單例模式執行緒
- Java 執行緒安全問題的本質Java執行緒
- ConcurrentHashMap執行緒安全機制以及原始碼分析HashMap執行緒原始碼
- Java——HashMap原始碼解析JavaHashMap原始碼
- 多執行緒併發安全問題詳解執行緒
- SpringMVC中出現的執行緒安全問題分析SpringMVC執行緒
- 5分鐘搞懂多執行緒安全問題執行緒
- 多執行緒的安全性問題(三)執行緒
- JUC之集合中的執行緒安全問題執行緒
- Python執行緒安全問題及解決方法Python執行緒
- RxJava原始碼解析(二)—執行緒排程器SchedulerRxJava原始碼執行緒
- HashMap原始碼解析、jdk7和8之後的區別、相關問題分析(多執行緒擴容帶來的死迴圈)HashMap原始碼JDK執行緒
- Android多執行緒之Handler、Looper與MessageQueue原始碼解析Android執行緒OOP原始碼
- Java併發包原始碼學習系列:執行緒池ThreadPoolExecutor原始碼解析Java原始碼執行緒thread