1. 程式人生 > >【java基礎 12】HashMap中是如何形成環形連結串列的?

【java基礎 12】HashMap中是如何形成環形連結串列的?

導讀:經過前面的部落格總結,可以知道的是,HashMap是有一個一維陣列和一個連結串列組成,從而得知,在解決衝突問題時,hashmap選擇的是鏈地址法。為什麼HashMap會用一個數組這連結串列組成,當時給出的答案是從那幾種解決衝突的演算法中推論的,這裡給出一個正面的理由:

1,為什麼用了一維陣列:陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難

2,為什麼用了連結串列:連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。連結串列的特點是:定址困難,插入和刪除容易

而HashMap是兩者的結合,用一維陣列存放雜湊地址,以便更快速的遍歷;用連結串列存放地址值,以便更快的插入和刪除!

一、環形連結串列的形成分析

那麼,在HashMap中,到底是怎樣形成環形連結串列的?這個問題,得從HashMap的resize擴容問題說起!

備註:本部落格中所示原始碼,均為java 7版本

HashMap的擴容原理:

 /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

當HashMap中的元素個數超過陣列大小(陣列總大小length,不是陣列中個數size)*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12(這個值就是程式碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的效能。

再看原始碼中,關於擴容resize()的實現:

   /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    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);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

備註:請注意這句話: newCapacity the new capacity, MUST be a power of two; must be greater than current capacity unless current  capacity is MAXIMUM_CAPACITY (in which case value is irrelevant)

在這裡面,又呼叫了一個函式transfer函式:

   /**
     * Transfers all entries from current table to newTable.
     */
    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;
            }
        }
    }
總得來說,就是拷貝舊的資料元素,從新新建一個更大容量的空間,然後進行資料複製!

那麼關於環形連結串列的形成,則主要在這擴容的過程。當多個執行緒同時對這個HashMap進行put操作,而察覺到記憶體容量不夠,需要進行擴容時,多個執行緒會同時執行resize操作,而這就出現問題了,問題的原因分析如下:

首先,在HashMap擴容時,會改變連結串列中的元素的順序,將元素從連結串列頭部插入。PS:說是為了避免尾部遍歷,這一部分不是本部落格的主要介紹內容,後面再說。

而環形連結串列就在這一時刻發生,以下模擬2個執行緒同時擴容。假設,當前hashmap的空間為2(臨界值為1),hashcode分別為0和1,在雜湊地址0處有元素A和B,這時候要新增元素C,C經過hash運算,得到雜湊地址為1,這時候由於超過了臨界值,空間不夠,需要呼叫resize方法進行擴容,那麼在多執行緒條件下,會出現條件競爭,模擬過程如下:

(額,我畫圖的功底一向不好,見諒見諒)

執行緒一:讀取到當前的hashmap情況,在準備擴容時,執行緒二介入


執行緒二:讀取hashmap,進行擴容


執行緒一:繼續執行


這個過程為,先將A複製到新的hash表中,然後接著複製B到鏈頭(A的前邊:B.next=A),本來B.next=null,到此也就結束了(跟執行緒二一樣的過程),但是,由於執行緒二擴容的原因,將B.next=A,所以,這裡繼續複製A,讓A.next=B,由此,環形連結串列出現:B.next=A; A.next=B 

二、總結

在這裡,只總結一個事兒,額,算是摘抄總結吧,就是在原始碼註釋中,發現擴容的時候,必須為2的指數,這是為什麼呢?

請點選此連結:HashMap擴容機制、執行緒安全  或者,自行學習hashmap的擴容機制

本篇部落格介紹環形連結串列的形成就先到這裡,下一篇部落格介紹怎麼判斷是否出現環形連結串列!