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原始碼解析(基於JDK1.7)HashMap原始碼JDK
- 多執行緒 HashMap 死迴圈 問題解析執行緒HashMap
- 深入解讀HashMap執行緒安全性問題HashMap執行緒
- 執行緒安全操作HashMap執行緒HashMap
- HashMap多執行緒併發問題分析HashMap執行緒
- Jdk1.7下的HashMap原始碼分析JDKHashMap原始碼
- HashMap jdk1.7和1.8原始碼剖析HashMapJDK原始碼
- SimpleDateFormat 執行緒安全問題ORM執行緒
- java執行緒安全問題Java執行緒
- 03 執行緒安全問題執行緒
- 執行緒池執行模型原始碼全解析執行緒模型原始碼
- 多執行緒-執行緒安全問題的產生原因分析以及同步程式碼塊的方式解決執行緒安全問題執行緒
- ArrayList 的執行緒安全問題執行緒
- 深入JAVA執行緒安全問題Java執行緒
- HashMap為何執行緒不安全HashMap執行緒
- 多執行緒,你覺得你安全了?(執行緒安全問題)執行緒
- 多執行緒下HashMap的死迴圈問題執行緒HashMap
- Java多執行緒中執行緒安全與鎖問題Java執行緒
- 從原始碼的角度解析執行緒池執行原理原始碼執行緒
- 執行緒安全使用 HashMap 的四種技巧執行緒HashMap
- parallelStream中的執行緒安全問題Parallel執行緒
- 所謂的執行緒安全問題執行緒
- Java原始碼解析 - ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java原始碼解析 ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java執行緒池ThreadPoolExecutor原始碼解析Java執行緒thread原始碼
- 多執行緒-生產者消費者問題程式碼2並解決執行緒安全問題執行緒
- Netty原始碼解析一——執行緒池模型之執行緒池NioEventLoopGroupNetty原始碼執行緒模型OOP
- ConcurrentHashMap原始碼解析,多執行緒擴容HashMap原始碼執行緒
- lambda中stream執行緒安全的問題執行緒
- 從FMDB執行緒安全問題說起執行緒
- Java 執行緒安全問題的本質Java執行緒
- 模板方法中的執行緒安全問題執行緒
- 單例模式執行緒安全reorder問題單例模式執行緒
- Java——HashMap原始碼解析JavaHashMap原始碼
- 多執行緒非同步安全,安全鎖的問題執行緒非同步
- HashMap為何執行緒不安全?HashMap,HashTable,ConcurrentHashMap對比HashMap執行緒
- 面試必問-幾種執行緒安全的Map解析面試執行緒
- 執行緒問題執行緒