jdk源碼研究1-HashMap

今天開始,研讀下jdk的經常使用類的一些源碼,下面是jdk中HashMap的研究。誠然,網上已經不少這方面的總結了,可是,我的只是想單純地把本身的理解過程進行記錄,大牛們就繞路吧,固然,歡迎扔磚頭。下面是大致的內容以下:java

1、哈希的概述算法

  一、哈希的概念api

  二、哈希要解決的問題數組

2、java中哈希的實現過程安全

  一、java中實現哈希的關鍵步驟數據結構

  二、關於resize過程的分析多線程

3、java中HashMap在併發時存在鏈表死循環問題。併發

 

1、哈希的概述函數

  一、數組和哈希源碼分析

  在說哈希這個數據結構前,先用數組這個耳聞能詳的數據結構進行引出:咱們知道,訪問數組的經常使用方法是經過數組遊標進行訪問,我只要持有數組某個元素對應的遊標,我就能在O(1)時間內獲取該元素。數組的這個性質讓它在查詢的時候速度很是快,而究其緣由,實際上是由於數組中元素位置和遊標時一一對應的,肯定遊標即肯定元素。這裏的遊標,就是哈希結構中的key,元素就是value。

  二、哈希是什麼

  上面說到了哈希的key以及value,那到底他們是什麼?能夠這樣理解哈希結構:key就是數組的遊標,value就是咱們要存儲的元素,哈希結構能夠理解爲數組結構的一種擴展,而數組則是特殊的哈希結構,在哈希結構中,咱們經過key能夠在O(1)時間內定位value。而哈希結構與數組的區別在於:哈希結構中,key不只僅是數字,它更能夠是對象;哈希結構中元素個數不像數組同樣不變,而是能夠動態變化的。

  三、如何實現哈希

  因此,其實實現哈希主要是要解決這些問題:首先,我要把對象進行值映射,這樣解決了key能夠是不一樣對象的問題;而後,把映射值進行壓縮,這樣保證對象映射的值集中在某個有限的區間內,內存上纔能有效利用這些值進行映射表的創建;最後,咱們須要在適當的時候擴展映射表的結構,這樣能夠解決元素個數動態的問題。  

  而後,下面咱們就研讀下java中的HashMap是如何解決上面提到的這些問題的

 

2、java 中的HashMap實現原理

  一、映射的解決方法

  在java中,最頂層的類Object 有一個方法:hashCode,下面是api截圖的說明:

  hashCode方法,就是把普通java 對象映射爲一個哈希嗎的方法,其實在算法底層來講,哈希碼是經過對象以及內存地址、對象建立當前時間等等數值來進行運算得出的一個數字,基本上,某段時間內的某個對象,哈希碼是獨一無二的。

  固然,上面的API說到,一個對象,若是經過equals 比較的結果是相等的話,應該保證hashCode也是相等的,這就要求咱們若是重寫equals 方法必須重寫hashCode方法。java API中這樣要求的緣由實際上是和哈希結構在插入數據時如何肯定要插入的key是否存在有關,具體後面說到map的put方法時再展開。

  二、hashCode的壓縮問題

  顯然,hashCode的範圍是很大的,不一樣對象組對應的hashCode對應的範圍可能會很大,例如對象一的hashCode是3,而後對象2的hashCode是30000,若是咱們直接用一個30000 + 1(假設遊標從0開始)大的數組來映射對應的元素(hashCode爲3的放到數組對應遊標爲3的地方,爲30000的放到遊標對應爲30000位置處),這樣,顯然中間不少位置是沒有元素的,這樣那部分空間就拜拜浪費了。因此,咱們須要一種算法,把各個對象的hashCode壓縮在一個必定的範圍內,而後,把hashCode壓縮以後的數字做爲數組的遊標,對應着原來的對象,這樣就解決了空間浪費的問題,同時又以數組來實現了哈希的結構。

  在java 中,其實它壓縮算法是下面這段代碼:

static int indexFor(int h, int length) {
        return h & (length-1);
    }

  至於爲何h & (length-1) 這個算法能夠有效保證把元素壓縮在一個範圍內並且能夠很大程度地避免發生哈希碰撞,更具體的緣由,能夠參開這篇博客:http://blog.csdn.net/qq_27093465/article/details/52207152,這裏不累贅展開。

  三、哈希衝突問題

  上面提到了一個概念叫作哈希衝突問題:當咱們在進行數據壓縮的時候,理想的哈希壓縮算法固然是,n個元素的hashCode恰好都壓縮在0-n-1的範圍,這樣就能最大程度地保證數組中的空間都被用到了,然而,對象之間的hashCode是毫無規律的,這種壓縮算法是不存在的。就是說,全部不一樣哈希碼並不能保證壓縮以後的映射壓縮值都不相等,並無找到一種算法能夠吧值都壓縮在一個恰當的範圍並且剛好都不相等,壓縮數據的範圍越小,衝突的概率越大,反之,概率越小,這種因爲壓縮hashCode以後形成的對應值相等的狀況就是哈希衝突問題。

  哈希衝突問題解決方案挺多,可是最經常使用的一種叫作拉鍊法,也是java 中的哈希結構所用的方法,它是一種鏈表數組的結構,看下面示意圖:

  上面就是利用一個數組長度爲5的鏈表數組來表示hashCode壓縮以後值分別爲15,36,41,24這個四個值。顯然,這種結構中,查找元素速度確定是沒有數組元素中利用遊標進行元素查找那麼快的,可是,只要對應的壓縮算法可以較好地分散元素,哈希衝突不嚴重時,查找某個元素仍是能夠保持爲O(1)的複雜度的,同時,也能保證空間較大的利用率。

3、java中put方法的具體執行過程

  有了上面這麼多的鋪墊,下面再對java的哈希結構中添加元素的過程進行簡單的總結。在java的HashMap中,執行添加算法大致要經歷如下步驟:首先進行對象與哈希值的映射,而後進行哈希碼的壓縮操做,再根據壓縮獲得的結果,找到鏈表數組的對應鏈表,看該位置是否存在了元素(不爲null),若是該遊標元素尚未聊表元素,直接插入;不然,遍歷該結果值對應的遊標的鏈表元素,進行鏈表的遍歷,利用eauql方法判斷元素是否存在,若是存在,則覆蓋舊值;不然,直接添加。 固然,在添加新元素前,須要進行元素個數的判斷,當元素個數超過負載因子*數組長度時,就要進行resize操做了。

  下面,對這個過程的更多細節的地方,咱們進行一一展開。

  一、put方法的源碼分析。先看看put這個方法是如何執行的:

public V put(K key, V value) {
    //若是key是null,直接調用特殊的putForNullKey方法進行元素添加
    if (key == null)
        return putForNullKey(value);
    //利用hash函數,計算哈希碼
    int hash = hash(key);
    //進行哈希碼的壓縮映射
    int i = indexFor(hash, table.length);
    //遍歷壓縮以後的映射值對應的遊標的鏈表元素,查看是否包含了新添加的key值。
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //若是存在 了key,直接替換舊value,並返回舊的value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    //不然,進行add元素的操做
    addEntry(hash, key, value, i);
    return null;
}

  put操做的主要操做步驟在註釋裏面也說了,下面就詳細針對put這個方法的addEntry方法進行詳細的總結。

 

  二、addEntry的過程。先看源碼和對應的註釋:

void addEntry(int hash, K key, V value, int bucketIndex) {
    //首先,判斷當前map中元素的個數是否超過了threshold這個臨界值,這個臨界值的計算方式是:負載因子*鏈表數組長度
    //若是超過了,則進行resize(擴容)操做,不然,直接執行createEntry操做(就是直接在鏈表末端插入新鏈表節點便可)
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //執行擴容操做
        resize(2 * table.length);
        //擴容操做執行以後,爲何要從新計算hash值?這個但願有大牛懂的話能夠告知下
        hash = (null != key) ? hash(key) : 0;
        //擴容以後,length發生了變化,因此bucketIndex須要從新計算
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

  這裏的addEntry操做,主要就是先判斷當前元素個數是否超過了一個臨界值,這個臨界值是鏈表數組*負載因子(默認是0.75),超過了的話就要執行擴容操做。不過本人有點不明白的地方是:爲何resize以後,要從新計算對象的hash值的?按道理,hashCode與擴容與否應該無直接關係吧?

  三、resize方法的執行過程。那麼resize又是怎麼執行的呢?仍是先看代碼吧:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //先判斷當前的數組容量是否超過了最大容量容許值,若是超過了,直接設置threshold位Integer.MAX_VALUE並不執行擴容操做
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //建立擴容新數組
    Entry[] newTable = new Entry[newCapacity];
    //下面這段代碼其實我也查了很久資料,發現網上並無相關的資料講解,
    //就是oldAltHashing和useAltHashing兩個值究竟是幹嗎的,有大佬知道很是但願能夠告知下
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    //transfer把舊元素統一移動到新的鏈表數組上去
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

  這段代碼整體不難看懂,不過本人其實也有一點疑問:就是oldAltHashing 和useAltHashing 的含義是什麼?這個但願有大牛懂的留言告知下,本人查了挺久也沒獲得相關的解釋。

  整體來講,resize過程就是建立一個雙倍大的新鏈表數組,並從新計算已有全部元素的壓縮哈希值,根據新的結果,從新調整哈希表。這個過程,實際上是挺耗費性能的。而具體的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);
            //把該節點插入到新數組的對應節點
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

  上面的transfer 過程,主要就是理解e.next = newTable[i];newTable[i] = e;e=next這三個語句:首先是把舊的元素的下一個節點指向新的鏈表數組的對應元素(一開始爲null),固然,舊元素的next元素須要一個next變量進行存儲;當下一個新元素也插入同一個位置時,重複上面這個過程的效果是:在舊數組中在後面的元素,在新擴容後數組對應每一個鏈表中,反而在前面了,至關於反轉了。若是還很差理解,看下面的示意圖:

  

  

3、關於HashMap中多線程下的安全問題

  在瞭解了HashMap的機制以後,順便提一個問題:HashMap是非線程安全的,這不只僅是由於它沒法保證數據的一致性,更會有可能造成聊表死循環,形成性能的極度浪費。出現這個問題的根本緣由實際上是:在transfer 中,執行了舊的鏈表節點指向新的數組元素時,其餘線程恰好執行到next = e.next時,此時,這個線程的next就會存儲了新的數組中對應的鏈表而不是舊的數組的原有鏈表了,當前線程繼續往下執行時,便會出現  節點的next指向自身的狀況,參考下面的圖進行理解:

  

  因此,HashMap是不能在多線程下使用的,它 不只會發生數據不一致的問題,並且會形成死循環!

相關文章
相關標籤/搜索