高並發下,HashMap會產生哪些問題?
阿新 • • 發佈:2018-09-07
hit 高並發 nsf ash ide don 原因 發生 機制
HashMap在高並發環境下會產生的問題
HashMap其實並不是線程安全的,在高並發的情況下,會產生並發引起的問題:
比如:
- HashMap死循環,造成CPU100%負載
- 觸發fail-fast
下面逐個分析下出現上述情況的原因:
HashMap死循環的原因
HashMap進行存儲時,如果size超過(當前最大容量*負載因子)時候會發生resize,首先看一下resize源代碼:
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方法是真正執行rehash的操作,容易在高並發時發生問題 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); } } }
HashMap死循環演示:
假如有兩個線程P1、P2,以及table[]某個節點鏈表為 a->b->null(a、b是HashMap的Entry節點,保存著Key-Value鍵值對的值)
P1先執行,執行完"Entry<K,V> next = e.next;"代碼後,P1發生阻塞或者其他情況不再執行下去,此時e=a,next=b
- P1阻塞後P2獲得CPU資源開始執行,由於P1並沒有執行完transfer(),table 和 threshold仍為原來的值,P2依舊會進行resize操作,並且P2順利執行完resize()方法,假設a、b節點仍然rehash到newTable[](註意,P1和P2中newTable[]不是同一個)中同一個節點鏈表中,則新的節點鏈表為 b->a->null
transfer(newTable); //P1阻塞在transfer方法中,沒有執行到下邊對 table 和 threshold 重新賦值的操作 table = newTable; threshold = (int)(newCapacity * loadFactor);
- P1又繼續執行"Entry<K,V> next = e.next;"之後的代碼,則newTable[i]的節點鏈表變化過程為:
- 第一次while循環,newTable[i]=a,鏈表為:b->a->null;此時e=b;
- 進入第二次循環,newTable[i]=b,鏈表為:b->a->a; 此時a<->a出現回路,e=a, while(e!=null)死循環
觸發fail-fast
一個線程利用叠代器叠代時,另一個線程做插入刪除操作,造成叠代的fast-fail。
public class TestFailFast {
private static final String USER_NAME_PREFIX = "User-";
// Key: User Name, Value: User Age
private static Map<String, Integer> userMap = new HashMap<>();
// ThreadA 用於向HashMap添加元素
static class ThreadA implements Runnable {
@Override
public void run() {
System.out.println("ThreadA starts to add user.");
for (int i = 1; i < 100000; i++) {
userMap.put(USER_NAME_PREFIX+i, i%100);
}
System.out.println("ThreadA done.");
}
}
// ThreadB 用於遍歷HashMap中元素輸出
static class ThreadB implements Runnable {
@Override
public void run() {
System.out.println("ThreadB starts to iterate.");
for (Map.Entry<String, Integer> user : userMap.entrySet()) {
System.out.println("UserName=" + user.getKey()
+ ", UserAge=" + user.getValue());
}
System.out.println("ThreadB done.");
}
}
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new ThreadA());
Thread threadB = new Thread(new ThreadB());
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.exit(0);
}
}
運行結果:拋出ConcurrentModificationException
ThreadA starts to add user.
ThreadB starts to iterate.
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
at concurrent.TestFailFast$ThreadB.run(TestFailFast.java:33)
at java.lang.Thread.run(Thread.java:748)
ThreadA done.
總結
HashMap並非線程安全,所以在多線程情況下,應該首先考慮用ConcurrentHashMap,避免悲劇的發生。
參考資料:
https://blog.csdn.net/chenxuegui1234/article/details/39646041
https://blog.csdn.net/u011716215/article/details/78601916
高並發下,HashMap會產生哪些問題?