本文要解決的問題: 最近無心中發現有不少對Map尤爲是HashMap的線程安全性的話題討論,在個人理解中,對HashMap的理解中也就知道它是線程不安全的,以及HashMap的底層算法採用了鏈地址法來解決哈希衝突的知識,可是對其線程安全性的認知有限,故寫這篇博客的目的就是讓和我同樣對這塊內容不熟悉的小夥伴有一個對HashMap更深的認知。node
哈希表 在數據結構中有一種稱爲哈希表的數據結構,它其實是數組的推廣。若是有一個數組,要最有效的查找某個元素的位置,若是存儲空間足夠大,那麼能夠對每一個元素和內存中的某個地址對應起來,而後把每一個元素的地址用一個數組(這個數組也稱爲哈希表)存儲起來,而後經過數組下標就能夠直接找到某個元素了。這種方法術語叫作直接尋址法。這種方法的關鍵是要把每一個元素和某個地址對應起來,因此若是當一組數據的取值範圍很大的時候,而地址的空間又有限,那麼必然會有多個映射到同一個地址,術語上稱爲哈希衝突,這時映射到同一個地址的元素稱爲同義詞。畢竟,存儲空間有限,因此衝突是不可避免的,可是能夠儘可能作到減小衝突。目前有兩種比較有效的方法來解決哈希衝突:算法
鏈地址法 開放地址法 這裏簡要說明一下開放地址法,顧名思義,就是哈希表中的每一個位置要麼存儲了一個元素要麼爲NULL。當數據比較多的時候,查找一個元素挺費事的,可是可使用探測的方法進行查找。這個話題與本主題關係不大,感興趣的小夥伴能夠自行研究。數組
鏈地址法 爲何要把鏈地址法單獨拿出來呢?由於後面有用。 鏈地址法的大概思想是:對於每一個關鍵字,使用哈希函數肯定其在哈希表中位置(也就是下標),若是該位置沒有元素則直接映射到該地址;若是該位置已經有元素了,就把該元素鏈接到已存在元素的尾部,也就是一個鏈表,並把該元素的next設置爲null。這樣的話,每一個哈希表的位置均可能存在一個鏈表,這種方式要查找某個元素效率比較高,時間複雜度爲O(1+a),a爲哈希表中每一個位置鏈表的平均長度。這裏須要假設每一個元素都被等可能映射到哈希表中的任意一個位置。安全
下面這張圖展現了鏈地址法的過程: HashMap HashMap底層實現 HashMap容許使用null做爲key或者value,而且HashMap不是線程安全的,除了這兩點外,HashMap與Hashtable大體相同,下面是官方API對HashMap的描述:數據結構
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.多線程
若是有多個線程對Hash映射進行訪問,那麼至少有一個線程會對哈希映射進行結構的修改:併發
結構上的修改是指添加或刪除一個或多個映射關係的任何操做;僅改變與實例已經包含的鍵關聯的值不是結構上的修改ide
那麼很顯然,當多個線程同時(嚴格來講不能稱爲同時,由於CPU每次只能容許一個線程獲取資源,只不過期間上很是短,CPU運行速度很快,因此理解爲同時)修改哈希映射,那麼最終的哈希映射(就是哈希表)的最終結果是不能肯定的,這隻能看CPU心情了。若是要解決這個問題,官方的參考方案是保持外部同步,什麼意思?看下面的代碼就知道了:函數
Map m = Collections.synchronizedMap(new HashMap(...)); 1 可是不建議這麼使用,由於當多個併發的非同步操做修改哈希表的時候,最終結果不可預測,因此使用上面的方法建立HashMap的時候,當有多個線程併發訪問哈希表的狀況下,會拋出異常,因此併發修改會失敗。ui
若是有多個線程執行put方法,並調用resize方法,那麼就會出現多種狀況,在轉移的過程當中丟失數據,或者擴容失敗,都有可能,因此從源碼的角度分析這也是線程不安全的。
Hashtable和ConcurrentHashMap Hashtable的底層實現 在介紹HashMap提到Hashtable是線程安全的,那麼H啊時table是如何實現線程安全的呢?有了上面的介紹,咱們直接從源碼中分析其線程安全性:
public synchronized V put(K key, V value) { // 保證value值不爲空,此處省略其代碼 // 保證key是不重複的,此處省略其代碼 //查過閾值則擴容,此處省略 // Creates the new entry. Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; return null; } 1 2 3 4 5 6 7 8 9 10 經過源碼能夠很明顯看到其put方法使用synchronized關鍵字,在線程中這是實現線程安全的一種方式,因此Hashtable是線程安全的。
ConcurrentHashMap的底層實現 ConcurrentHashMap支持徹底併發的對哈希表的操做,ConcurrentHashMap聽從了和Hashtable同樣的規範,這裏指的是線程安全的規範,可是其底層的實現與Hashtable並不一致。ConcurrentHashMap底層採用的鎖機制,執行put方法的線程會得到鎖,只有當此線程的put方法執行結束後纔會釋放鎖,根據多線程的知識,得到鎖的線程會通知其餘試圖操做put方法的線程,並通知其餘線程出於等待狀態,直到釋放鎖後,其餘線程纔會去從新競爭鎖。這一點保證了ConcurrentHashMap的線程安全。
注:這裏涉及到了線程鎖的知識,若是對這塊內容不熟悉,能夠參考API。 引用一段官方API對ConcurrentHashMap的描述:
A hash table supporting full concurrency of retrievals and adjustable expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access. This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.
從這段描述能夠看出,ConcurrentHashMap其實是Hashtable的升級版,除了具有線程安全外還增長了迭代器快速失敗行爲的異常處理,也就是說,經過ConcurrentHashMap對Iterator迭代器結構的修改不會拋出異常,而Hashtable會拋出異常,於是就Hashtable來講,若是迭代器修改了映射結構,那麼遍歷的結果是不肯定的,而ConcurrentHashmap支持之容許一個線程對迭代器的映射結構進行修改。
那麼咱們接着從源碼的角度分析ConcurrentHashMap是如何實現線程安全的:
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); } 1 2 3 4 5 6 7 8 9 10 11 ConcurrentHashMap把要放入的數據分紅了多段數據,而後對每段的put操做進行加鎖,下面看一下ensureSegment方法:
private Segment<K,V> ensureSegment(int k) { final Segment<K,V>[] ss = this.segments; long u = (k << SSHIFT) + SBASE; // raw offset Segment<K,V> seg; if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { Segment<K,V> proto = ss[0]; // use segment 0 as prototype int cap = proto.table.length; float lf = proto.loadFactor; int threshold = (int)(cap * lf); HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap]; if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck Segment<K,V> s = new Segment<K,V>(lf, threshold, tab); while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; } } } return seg; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 這段代碼的做用就是根據給定的索引,返回某個具體的Segment,而後根據返回的Segment(塊)加鎖執行put方法。 再看s.put()方法:
final V put(K key, int hash, V value, boolean onlyIfAbsent) { HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { //此處省略詳細的處理過程 } } finally { unlock(); } return oldValue; } 1 2 3 4 5 6 7 8 9 10 11 12 在上面的源碼中出現了Segment s,咱們來看看它何方神聖:
Segments are specialized versions of hash tables. This subclasses from ReentrantLock opportunistically, just tosimplify some locking and avoid separate construction.
從這段註釋中能夠發現每次執行ConcurrentHashMap的put方法都是調用s.put()方法的,而Segments對象是一個繼承了ReentrantLock鎖對象的子類,那麼剩下的就很清晰了,每個Segments都有一個鎖,只有執行完上面try語句塊中的代碼纔會釋放鎖,從而保證了多線程併發訪問的安全性。
下面來看看ConcurrentHashMap的get方法
public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 get操做會經過key找到哈希表的哈希值,根據哈希值定位到某個Segment,而後再從Segment中返回value
小結 HashMap採用鏈地址法解決哈希衝突,多線程訪問哈希表的位置並修改映射關係的時候,後執行的線程會覆蓋先執行線程的修改,因此不是線程安全的 Hashtable採用synchronized關鍵字解決了併發訪問的安全性問題可是效率較低 ConcurrentHashMap使用了線程鎖分段技術,每次訪問只容許一個線程修改哈希表的映射關係,因此是線程安全的 本文大部份內容轉自這篇博客http://blog.csdn.net/sbq63683210/article/details/51679790