PS:不得不說Java編程思想這本書是真心強大..算法
學習內容:編程
1.HashMap<K,V>在多線程的狀況下出現的死循環現象api
當初學Java的時候只是知道HashMap<K,V>在併發的狀況下使用的話,會出現線程安全問題,可是一直都沒有進行深刻的研究,也是最近實驗室的徒弟在問起這個問題的緣由以後,纔開始進行了一個深刻的研究.安全
那麼這一章也就僅僅針對這個問題來講一下,至於如何使用HashMap這個東西,也就不進行介紹了.在面對這個問題以前,咱們先看一下HashMap<K,V>的數據結構,學過C語言的,你們應該都知道哈希表這個東西.其實HashMap<K,V>和哈希表我能夠說,思想上基本都是同樣的.數據結構
這就是兩者的數據結構,上面那個是C語言的數據結構,也就是哈希表,下面的則是Java中HashMap<K,V>的數據結構,雖然數據結構上稍微有點差別,不過思想都是同樣的.咱們仍是以HashMap<K,V>進行講解,咱們知道HashMap<K,V>有一個叫裝載因子的東西,默認狀況下HashMap<K,V>的裝載因子是75%這是在時間和空間上尋求的一個折衷.那麼什麼是所謂的裝載因子,裝載因子實際上是用來判斷當前的HashMap<K,V>中存放的數據量,若是咱們存放的數據量大於了75%,那麼HashMap<K,V>就須要進行擴容操做,擴容的空間大小就是原來空間的兩倍.可是擴容的時候須要reshash操做,其實就是講全部的數據從新計算HashCode,而後賦給新的HashMap<K,V>,rehash的過程是很是耗費時間和空間的,所以在咱們對HashMap的大小進行控制的時候,應該要進行至關的考慮.還有一個誤區(HashMap<K,V>可不是無限大的.)多線程
簡單介紹完畢以後,就說一下正題吧.其實在單線程的狀況下,HashMap<K,V>是不會出現問題的.可是在多線程的狀況下也就是併發狀況下,就會出現問題.若是HashMap<K,V>的容量很大,咱們存入的數據不多,在併發的狀況下出現問題的概率仍是很小的.出現問題的主要緣由就是,當咱們存入的數據過多的時候,尤爲是須要擴容的時候,在併發狀況下是很容易出現問題.針對這個現象,咱們來分析一下.併發
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]; boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing; transfer(newTable, rehash); //transfer函數的調用 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
上面說過,但HashMap<K,V>的空間不足的狀況下,須要進行擴容操做,所以在Java JDK中須要使用resize()函數,Android api中是找不到resize函數的,Android api是使用ensureCapacity來完成調用的..原理其實都差很少,我這裏仍是隻說Java JDK中的..其實在resize()這個過程當中,在併發狀況下也是不會出現問題的..
高併發
關鍵問題是transfer函數的調用過程..咱們來看一下transfer的源碼..
學習
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { //這裏纔是問題出現的關鍵.. while(null != e) { Entry<K,V> next = e.next; //尋找到下一個節點.. if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //從新獲取hashcode e.next = newTable[i]; newTable[i] = e; e = next; } } }
transfer函數實際上是在併發狀況下致使死循環的因素..由於這裏涉及到了指針的移動的過程..transfer的源碼一開始我並有徹底的看懂,主要仍是newTable[i]=e的這個過程有點讓人難理解..其實這個過程是一個很是簡單的過程..咱們來看一下下面這張圖片..
這是在單線程的正常狀況下,當HashMap<K,V>的容量不夠以後的擴容操做,將舊錶中的數據賦給新表中的數據.正常狀況下,就是上面圖片顯示的那樣.新表的數據就會很正常,而且還須要說的一點就是,進行擴容操做以後,在舊錶中key值相同的數據塊在新表中數據塊的鏈接方式會逆向.就拿key = 3和key = 7的兩個數據塊來講,在舊錶中是key = 3 的數據塊指向key = 7的數據塊的,可是在新表中,key = 7的數據塊則是指向了key = 3的數據塊key = 5 的數據塊不和兩者發生衝突,所以就保存到了 i = 1 的位置(這裏的hash算法採用 k % hash.size() 的方式).這裏採用了這樣簡單的算法無非是幫助咱們理解這個過程,固然在正常狀況下算法是不可能這麼簡單的.
這樣在單線程的狀況下就完成了擴容的操做.其中不會出現其餘的問題..可是若是是在併發的狀況下就不同了.併發的狀況出現問題會有不少種狀況.這裏我簡單的說明倆種狀況.咱們來看圖。
這張圖可能有點小,你們能夠經過查看圖像來放大,就可以看清晰內容了...
這張圖說明了兩種死循環的狀況.第一種相對而嚴仍是很容易理解的.第二種可能有點費勁..可是有一點咱們須要記住,圖中t1和t2拿到的是同一個內存單元對應的數據塊.而不是t1拿到了一個獨立的數據塊,t2拿到了一個獨立的數據塊..這是不對的..之因此發生系循環的緣由就是由於拿到的數據塊是同一個內存單元對應的數據塊.這點咱們須要注意..正是由於在高併發的狀況下線程的工做方式是不肯定的,咱們沒法預知線程的工做狀況.所以在高併發的狀況下,咱們不要使用多線程對HashMap<K,V>進行操做,不然咱們都不知道究竟是哪裏出了問題.
可能看起來很複雜,可是隻要去思考,仍是感受蠻簡單的,我這只是針對兩個線程來分析了一下死循環的狀況,固然發生死循環的問題不只僅只是這兩種方式,方式可能會有不少,我這裏只是針對了兩個類型進行了分析,目的是方便你們理解.發生死循環的方式毫不僅僅只是這兩種狀況.至於其餘的狀況,你們若是願意去了解,能夠本身再去研磨研磨其餘的方式.按照這種思路分析,仍是能研磨出來的.而且這仍是兩個線程,若是數據量很是大,線程的使用還比較多,那麼就更容易發生死循環的現象.所以這就是致使HashMap<K,V>在高併發下致使死循環的緣由.
雖然咱們都知道當多線程對Map進行操做的時候,咱們只須要使用ConcurrentHashMap<K,V>就能夠了.可是咱們仍是須要知道爲何HashMap<K,V>在高併發的狀況下不可以那樣去使用.學同樣東西,不只僅要知道,並且還要知道其中的緣由和道理.