內容
網上很多資料都詳細地講解了HashMap底層的實現,但是講到HashMap的並發操作不是線性安全時,往往一筆帶過:在多個線程並發擴容時,會在執行transfer()方法轉移鍵值對時,造成鏈表成環,導致程序在執行get操作時形成死循環。
對於沒有研究過該過程的童鞋,很難費解這句話的含義。下面筆者分四個小節帶着大家共同研究一下JDK1.7和JDK1.8版本下HashMap的線性不安全是怎么造成的,詳細探究鏈表成環的形成過程。如果對於HashMap底層的put、get操作不清楚,建議先學習參考1中的內容。
適合人群
Java進階
說明
轉載請說明出處:探究HashMap線性不安全(三)——死循環的產生
參考
1、https://www.toutiao.com/i6544826418210013700/ HashMap底層數據結構原理
2、https://www.toutiao.com/i6545790064104833539/ 為什么HashMap非線程安全
3、https://blog.csdn.net/qq_32182461/article/details/81152025 hashmap並發情況下的成環原因(筆者認為該文是一種誤解)
正文
本節將探究環形鏈表是如何在hashMap查詢時產生死循環的。
以上節為例,當hashMap對象查找一個不為空的Key時,會執行getEntry(key)方法。
1 public V get(Object key) { 2 //key為null時,從index為0的鏈表中查找數據
3 if (key == null) 4 return getForNullKey(); 5 //key不為null是查找數據
6 Entry<K,V> entry = getEntry(key); 7 //如果未查找到key對應的值,那么entry為null,get方法會返回null。
8 return null == entry ? null : entry.getValue(); 9 }
getEntry(key)方法會計算Key的hash值,然后通過indexFor(hash, table.length)對hash值取模求得key對應table數組的下標index。如果這個Key對應的table數組下標為3,那么線程會在下標3處的環形鏈表上遍歷檢索目標key對應的值。當key對應的鍵值對不存在,線程將進入死循環,代碼如下所示:
1 final Entry<K,V> getEntry(Object key) { 2 //計算key的hash值
3 int hash = (key == null) ? 0 : hash(key); 4 //通過indexFor(hash, table.length)求得key對應的index,然后遍歷index下表的鏈表
5 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 6 e != null; 7 e = e.next) { 8 Object k; 9 //如果鏈表中不存在對應key的鍵值對,且鏈表為環形,那么當前線程將在for語句中產生死循環。
10 if (e.hash == hash &&
11 ((k = e.key) == key || (key != null && key.equals(k)))) 12 return e; 13 } 14 return null; 15 }
下面寫一個測試方法,使用多線程對HashMap進行並發寫操作,並進行單線程讀來演示死循環的產生。
1 public class TestMain { 2 public static void main(String[] args) throws InterruptedException { 3 final HashMap<String, String> map = new HashMap<>(); 4 System.out.println("begin"); 5 int num = 15; 6 final CountDownLatch countDownLatch = new CountDownLatch(num); 7 //開始並發put操作
8 for (int i = 0; i < num; i++) { 9 final Integer integer = i; 10 new Thread() { 11 @Override 12 public void run() { 13 for (int j = 0; j < 100; j++) { 14 map.put("a" + integer + j, "b" + integer + j); 15 } 16 countDownLatch.countDown(); 17 } 18 }.start(); 19 } 20 //等待所有線程完成寫入操作
21 countDownLatch.await(); 22 System.out.println(map.size()); 23 //循環讀取map中不存在的key,如果生成了環形鏈表,當key的hash值匹配到該鏈表時,發生死循環
24 for (int m = 0; m < 200; m++) { 25 System.out.println("c" + m + ": " + map.get("c" + m)); 26 } 27 } 28 }
截圖如下(筆者大概實驗了10次,模擬出次故障):
至此,JDK1.7中HashMap的線性不安全特性已經論證完畢。下一節,將探究JDK1.8中HashMap的線性不安全特性。