HashMap學習筆記

HashMap的基本數據結構是什麼?

HashMap是基於數組+鏈表+紅黑樹(JDK1.8)實現的。

主幹爲一個存儲Key-Value鍵值對的數組,存儲位置由key經過Hash算法生成,衝突時以頭插法的形式生成鏈表,當鏈表長度大於8時鏈表結構轉換爲紅黑樹。

HashMap有哪幾個關鍵的變量?分別起什麼作用?

關鍵變量:1)HashMap的當前長度Capacity,默認值爲16。2)容量因子Loadfactor,默認值爲0.75。3)最大有效容量Threshold。Threshold=Capacity×Loadfactor。每次做完新增鍵值對操作會拿當前鍵值數與Threshold進行比較,如果當前鍵值數>=Threshold,對HashMap執行擴容操作。

HashMap鏈表結構有什麼特點?

即使HashMap定位哈希桶索引位置的算法再合理也無法避免衝突,所以同一個哈希桶會出現鏈表過長的情況,一旦出現拉鍊過長,則會嚴重影響HashMap的性能,所以在Java8中,對數據結構做了進一步的優化,引入了紅黑樹。當鏈表長度太長(默認超過8)時,鏈表就轉換爲紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能。

HashMap如何實現擴容?

在一個PUT操作完成之前,會拿HashMap的當前數據量與最大容量進行比較,如果HashMap.Size>=threshold,HashMap會執行擴容操作。擴容主要通過Resize()方法來實現:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; //定義舊錶並把原本的賦值給他
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//如果舊錶容量爲null就初始0
    int oldThr = threshold;//舊錶的閥值
    int newCap, newThr = 0;//定義新表的容量和新表的閥值
    //進入條件:正常擴容  
    if (oldCap > 0) {//如果舊錶容量大於0,這個情況就是要擴容了
        //進入條件:已達到最大,無法擴容
        if (oldCap >= MAXIMUM_CAPACITY) {//如果容量已經大於等於1<<30
            threshold = Integer.MAX_VALUE;//設置閥值最大
            return oldTab;//直接返回原本的對象(無法擴大了)
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >=DEFAULT_INITIAL_CAPACITY)
            //舊錶容量左移一位<<1,且移動之後處於合法的範圍之中。新表容量擴充完成
            newThr = oldThr << 1; // 新表的閥值也擴大一倍。
        //進入條件:初始化的時候使用了自定義加載因子的構造函數
        } else if (oldThr > 0) 
            // 這裏如果執行的情況是原表容量爲0的時候,但是閥值又不爲0。
            //hashmap的構造函數不同(需要設置自己的加載因子)的時候會觸發。
            newCap = oldThr;
        //進入條件:調用無參或者一個參數的構造函數進入默認初始化
        else {
            // 如果HashMap默認構造就會進入下面這個初始化,第一次put也會進入下面這一塊。
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//初始化完成。
        }
    //進入條件:初始化的時候使用了自定義加載因子的構造函數
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;//新表容量*加載因子
        newThr = (newCap < MAXIMUM_CAPACITY && 
        ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;//確定新的閥值
    //開始構造新表
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //進入條件:原表存在
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {//開始遍歷
                oldTab[j] = null;//舊錶原本置空
                    if (e.next == null)//不存在下個節點,也就是目前put的就是鏈表頭
                        newTab[e.hash & (newCap - 1)] = e;//把該對象賦值給新表的某一個桶中
                    //進入條件:判斷桶中是否已紅黑樹存儲的。如果是紅黑樹存儲需要寧做判斷
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //進入條件:如果桶中的值是合法的,也就是不止存在一個,也沒有觸發紅黑樹存儲
                    else {
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;//獲取下一個對象信息
                            //因爲桶已經擴容了兩倍,所以以下部分是按一定邏輯的把一個鏈表拆分爲兩個鏈表,放入對應桶中。
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

這個方法實現了兩個操作:1,新建一個Entry空數組,長度是原數組的2倍。2,遍歷原數組,將所有的Entry重新執行Hash運算後存到新的數組。

PUT方法如何實現?

Java8與Java7相比HashMap有什麼改進?

1.JDK1.8引入了紅黑樹,當鏈表長度大於8時,鏈表結構改轉換爲紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能。

2.JDK1.8優化了高位運算,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),提高數據分佈的離散性。

3.JDK1.8在擴容HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,而是通過新增的一個位的值來確定bucket的位置,省去了重新計算hash值的時間,由於新增的一位的值是0還是1是隨機的,所以還能確保數據離散性。

HashMap爲什麼不是線程安全型的?

HashMap在擴容的時候需要執行兩個步驟:1,新建一個Entry空數組,長度是原數組的2倍。2,遍歷原數組,將所有的Entry重新執行Hash運算後存到新的數組。在高併發的情況下在將原數組數據複製到新數組的過程中容易出現環形鏈表導致死循環。

在高併發情況下如何實現線程安全的Map?

由於HashMap在高併發時在做插入操作可能會出現環形鏈表,所以在多線程的系統中不能使用HashMap,要避免這個問題有幾個辦法,比如改用HashTable或者使用Collections.sychronizeMap,但是這兩種方式無論是讀操作還是寫操作都會給整個集合加鎖,導致同一時間的其他操作被阻塞,這很影響性能,所以在高併發情況下,爲了兼顧線程安全和性能,通常會使用ConcurrentHashMap。

ConcurrentHashMap怎麼保證線程安全?

從數據結構來說ConcurrentHashMap是一個二級哈希表,在一個總的哈希表下面有若干個子哈希表。

在ConcurrentHashMap中,每一個Segment都是一個獨立的個體,在高併發情況下,不同Segment之間的併發操作互不影響,同一Segment的讀寫鎖可以併發執行的,只有對同一個Segment併發寫入的時候才需要上鎖,而且鎖只是針對Segment,這樣在保證線程安全性的同時降低了鎖的粒度,讓併發操作更有效率

ConcurrentHashMap怎麼實現高性能讀寫?

get方法:

1.爲輸入的key做hash運算,得到hash值。

2.通過hash值,定位到對應的Segment對象。

3.再次通過Hash值定位到Segment中數組具體位置(這一步相當於HashMap的get操作)。

put方法:

1.爲輸入key做hash運算,得到hash值。

2.通過hash值,定位到對應的Segment對象。

3.獲得可重入鎖。

4.再次通過hash值,定位到Segment中數組的具體位置。

5.插入或覆蓋HashEntry對象。(4.5相當於HashMap的put操作)

6.釋放鎖。

ConcurrentHashMap如何確保一致性?

由於每一個Segment在put操作的時候會單獨加鎖,這樣的結構通常會在計算總量的時候出現一致性問題。ConcurrentHashMap的size方法是一個嵌套循環,邏輯如下:

1.遍歷所有的Segment。

2.把Segment的數據累加起來。

3.把Segment的修改次數累加起來。

4.判斷所有的Segment的總修改次數是否大於上一次的總修改次數。如果大於,說明統計過程中有修改,重新計算,嘗試次數+1;如果不是,說明沒有修改,統計結束。

5.如果嘗試次數超過閥值,則對每一個Segment加鎖,再重新統計。

6.再次判斷所有Segment的總修改次數是否大於上一次的修改總數,由於已經加鎖,次數一定和上一次相等。

7.釋放鎖,統計結束。

這也是樂觀鎖悲觀鎖的思想,先樂觀的假設沒有併發的修改,當嘗試到一定的次數之後,才悲觀的認爲有修改,鎖住所有的Segment保證強一致性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章