---恢復內容開始---
前言:大多數javaer都知道HashMap是線程不安全的,多線程環境下數據可能會發生錯亂,一定要謹慎使用。這個結論是沒錯,可是HashMap的線程不安全遠遠不是數據臟讀這么簡單,它還有可能會發生死鎖,造成內存飆升100%的問題,情況十分嚴重(別問我是怎么知道的,我剛把機器重啟了一遍!)今天就來探討一下這個問題,HashMap在多線程環境下究竟會發生什么?
一:模擬程序
溫馨提示:咳咳,以下代碼需在家長陪同下使用,非戰斗人員請速速退場,否則帶來的一切后果請自己負責!
言歸正傳,我們先來寫個程序先:
import java.util.HashMap; import java.util.Map; /** * Created by Yiron on 3/30 0030. */ public class HashMapManyThread { static Map<String,String > map =new HashMap<String, String>(16);//初始化容量 public static class TestHashMapThread implements Runnable{ int start=0; public TestHashMapThread(int start){ this.start=start; } @Override public void run() { for (int i = 0; i <100000 ; i+=2) { System.out.println("--puting----"); map.put(Integer.toString(i),String.valueOf(Math.random()*100)); } } } public static void main(String[] args) throws InterruptedException { Thread[] threads =new Thread[100]; for (int i = 0; i <threads.length ; i++) { threads[i]=new Thread(new TestHashMapThread(i)); } for (int i = 0; i <100 ; i++) { threads[i].start(); } System.out.println(map.size()); } }
上面的程序開了100個線程去訪問給HashMap去put不同的值,如果是線程安全的,最后肯定會輸出5000,可惜事與願違,在嘗試了幾次以后,竟然程序給卡死了,緊接着打開任務管理器,發現cpu飆升至100%,而內存使用也有88%,簡直喪心病狂!無奈下只能重啟!
二:原因分析
在cmd中打開,然后輸入jps,可以查看所有的java進程,然后可以看到所有的線程都在運行中,一直在無限循環狀態,可以看到拋異常在at java.util.HashMap.put(HashMap.java:374)行,我們打開374行來看看:
以下是put方法的源碼:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { //374行 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; }
可以看到這里是遍歷數組的過程,其中遍歷它的元素過程中,有一個e.next也就是指針往下移動,這里就很容易出現問題了。假如我們有兩個線程Thread1和Thread2,假如在遍歷的過程中,Thread1此時在鏈表的節點上e1,next指針會下一層指向e2;而此時Thread2遍歷在e2節點上,它往回遍歷next指針指向e1,那么此時的鏈表結構就被破壞了,形成了雙向指針,構成了一個閉環(如圖所示),就造成“死鎖了”,我們來復習一下造成死鎖的4個條件。
三:死鎖的四個條件
1)互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程用畢釋放。
2)請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。
3)不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
4)環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1占用的資源;P1正在等待P2占用的資源,……,Pn正在等待已被P0占用的資源。
我們來分析一下鏈表的互相引用符不符合上面四個條件:
①互斥條件:鏈表上的節點同一時間此時被兩個線程占用,兩個線程占用訪問節點的權利,符合該條件
②請求和保持條件:Thread1保持着節點e1,又提出了占用節點e2(此時尚未釋放e2);而Thread2此時占用e2,又提出了占用節點e1,Thread1占用着Thread2接下來要用的e1,而Thread2又占用着Thread1接下來要用的e2,符合該條件
③:不剝奪條件:線程是由自己的退出的,此時並沒有任何中斷機制(sleep或者wait方法或者interuppted中斷),只能由自己釋放,滿足條件
④:環路等待條件:e1、e2、e3等形成了資源的環形鏈條,滿足該條件
五:解決方法
5.1:使用Collections.synchronizedMap(Map map)方法,可以將HashMap變成一個同步的容器(擁有鎖限制的同步機制)
static Map<String,String > map = Collections.synchronizedMap(new HashMap<String, String>());
synchronizedMap這個方法的原理的話,其實是把這個參數里面的hashMap注入到Collections的內部維護着的一個成員變量Map中,
final Object mutex;
public V put(K key, V value) { synchronized(mutex) {return m.put(key, value);} }
其中的mutex,是個不可變的成員變量,通過synchronized這個同步鎖塊就把整個代碼鎖住了,從而加上了同步規則。這個方法優點是簡單粗暴,缺點就是性能不是很好,因為是阻塞的方式。
5.2:使用concurrentHashMap
static Map<String,String > map = new ConcurrentHashMap<String, String>();
這個方式是使用ConcurrentHashMap,它是線程安全的,
public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, false); } V put(K key, int hash, V value, boolean onlyIfAbsent) { lock();//上鎖 try { int c = count; if (c++ > threshold) // ensure capacity rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile } return oldValue; } finally { unlock(); } }
可以看到,concurrentHashMap的put方法是加鎖的,它是同步的(采用了ReentrantLock可重入鎖),可以保證線程安全。
六:總結
本文分析了HashMap在並發環境下的嚴重的問題,並沒有我們想象中的那么輕易和簡單,會造成的嚴重的cpu飆升問題,從而產生內存泄露,所以在多線程的環境下一定要慎重慎重!最好不要用,可以取而代之用ConcurrentHashMap,它的內部數據結構與HashMap迥然不同,可以保證線程安全。