在多線程環境中,使用HashMap進行put操作時會引起死循環,導致CPU使用接近100%,下面通過代碼分析一下爲什麼會發生死循環。
首先先分析一下HashMap的數據結構:HashMap底層數據結構是有一個鏈表數據構成的,HashMap中定義了一個靜態內部類作爲鏈表,代碼如下(與本文無關的代碼省略):
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } 、 }
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table;
之所以會導致HashMap出現死循環是因爲多線程會導致HashMap的Entry節點形成環鏈,這樣當遍歷集合時Entry的next節點用於不爲空,從而形成死循環
單添加元素時會通過key的hash值確認鏈表數組下標
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
//確認鏈表數組位置
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//如果key相同則覆蓋value部分
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加鏈表節點
addEntry(hash, key, value, i);
return null;
}
下面看一下HashMap添加節點的實現
void addEntry(int hash, K key, V value, int bucketIndex) {
//bucketIndex 通過key的hash值與鏈表數組的長度計算得出
Entry<K,V> e = table[bucketIndex];
//創建鏈表節點
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//判斷是否需要擴容
if (size++ >= threshold)
resize(2 * table.length);
}
以上部分的實現不會導致鏈路出現環鏈,環鏈一般會出現HashMap擴容是,下面看看擴容的實現:
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];
transfer(newTable);//可能導致環鏈
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
下面transfer的實現
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
這個方法的目的是將原鏈表數據的數組拷到新的鏈表數組中,拷貝過程中如果形成環鏈的呢?下面用一個簡單的例子來說明一下:
public class InfiniteLoop {
static final Map<Integer, Integer> map = new HashMap<Integer, Integer>(2, 0.75f);
public static void main(String[] args) throws InterruptedException {
map.put(5, 55);
new Thread("Thread1") {
public void run() {
map.put(7, 77);
System.out.println(map);
};
}.start();
new Thread("Thread2") {
public void run() {
map.put(3, 33);
System.out.println(map);
};
}.start();
}
}
下面通過debug跟蹤調試來看看如果導致HashMap形成環鏈,斷點位置:
- 線程1的put操作
- 線程2的put操作
- 線程2的輸出操作
- HashMap源碼transfer方法中的第一行、第六行、第九行
測試開始
- 使線程1進入transfer方法第一行,此時map的結構如下
2. 使線程2進入transfer方法第一行,此時map的結構如下:
3.接着切換回線程1,執行到transfer的第六行,此時map的結構如下:
4.然後切換回線程2使其執行到transfer方法的第六行,此時map的結夠如上
5.接着切換回線程1使其執行到transfer方法的第九行,然後切換回線程2使其執行完,此時map的結構如下:
6.切換回線程1執行循環,因爲線程1之前是停在HashMap的transfer方法的第九行處,所以此時transfer方法的節點e的key=3,e.next的key=7
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);//線程1等線程2執行結束後
//從此處開始執行
//此時e的key=3,e.next.key=7
//但是此時的e.next.next的key=3了
//(被線程2修改了)
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
下面線程1開始執行第一次循環,循環後的map結構如下:
接着執行第二次循環:e.key=7,e.next.key=3,e.next.next=null
接着執行第三次循環,從而導致環鍊形成,map結構如下
並且此時的map中還丟失了key=5的節點