Java 容器篇 Map (二)

Map

HashMap

HashMap KV鍵值對 數據結構見下圖。HashMap的插入的過程是首先計算key的hash值然後跟數組長度做 與運算 得到一個索引值
插入數組的這個索引值進去,如果當前這個索引值沒有node直接CAS插入,如果當前這個有node就是發生了hash碰撞之後就加synchronized的鎖在鏈表後面加入這個node或者紅黑樹加這個node。

1.8的HashMap的數據結構就是 數組 + 鏈表 + 紅黑樹 在鏈表長度超過8的時候並且map的size超過64的時候會轉換成紅黑樹。
鏈表轉紅黑樹不是單向的,在紅黑樹的節點小於6的情況下會從紅黑樹轉換成鏈表,這個操作在擴容(resize)的時間進行。

鏈表轉換紅黑樹邏輯

    // 鏈表轉紅黑樹的閾值
    static final int TREEIFY_THRESHOLD = 8;
    // 紅黑樹轉回鏈表的閾值
    static final int UNTREEIFY_THRESHOLD = 6;
    // 轉紅黑樹對size最小的要求
    static final int MIN_TREEIFY_CAPACITY = 64;

爲什麼要從紅黑樹轉回鏈表這個我也不懂?有懂的大佬也可以評論區解析下。

紅黑樹的特性
  1. 每個節點或者是黑色,或者是紅色。
  2. 根節點是黑色。
  3. 每個葉子節點(NIL)是黑色。 [注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!]
  4. 如果一個節點是紅色的,則它的子節點必須是黑色的。
  5. 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。[這裏指到葉子節點的路徑]

補充一些概念

  1. hash值相等的equals不一定相等。
  2. equals相等的話hash這個一般來說一定要相等,除非你亂搞。
  3. hashMap 沒有put之前都是不會創建對應的node節點的,算是中lazy加載吧。
  4. 默認的加載因子是0.75 這個東西沒啥事就別改了。
  5. hashmap的擴容機制1.7跟1.8的也是不一樣。1.7的擴容從舊的移動到新的數組然後重新計算hash值。

擴容機制

1.7的擴容機制

擴容的時候需要重新Hash取模然後再重新放入新的數組裏面

1.8的擴容機制

用一位來確定是在原地位置還是原地+OldCap位置 擴容參考

1.8HashMap數據結構圖
HashMap數據結構圖

Hashtable

這個就很厲害了,都是synchronized基本上所有對外的方法的搞得性能很低不建議用。

ConcurrentHashMap

這個東西講起來就更厲害了,HashMap是不安全的這個是一個安全的Map。那他這個比HashTable強在哪裏呢?

讀是沒有加鎖的可以併發的去讀取,put的時候 synchronized + CAS 去掉了1.7的分段鎖這種概念,如果索引到的數組上爲null直接CAS插入如果有值則加synchronized 然後進行插入,插入的時候不允許key爲null 或者value爲null 。1.8的鎖粒度是非常小類似mysql的行數,它也是隻鎖定了一個node節點。所以併發操作性能很高。

因爲是分段鎖 插入、讀取、更新都是可以併發執行但是有些要鎖住全局的size()和containsValue()
爲什麼放棄ReentrantLock(1.7的分段鎖)用synchronizedConcurrentHashMap 1.8爲什麼要使用CAS+Synchronized取代Segment+ReentrantLock

initTable()方法

/**
 * 用於控制table初始化和resize的參數
 * -1表示初始化
 * 其他負數+1表示正在進行resize的線程數,爲了與-1區別開
 * 0代表默認狀態
 * 在初始化之後,該值表示下次需要resize時map內元素的個數
 */
private transient volatile int sizeCtl;
// 初始化時與sizeCtl值相等,爲0
private static final long SIZECTL;
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
            	// 如果正在初始化中當前線程做出讓步
                Thread.yield(); // lost initialization race; just spin
                // CAS設置如果設置成功就去更新不成功繼續自旋
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

計數 addCount

這個方法一共做了兩件事,更新baseCount的值,檢測是否進行擴容。

在Java 7中ConcurrentHashMap對每個Segment單獨計數,想要得到總數就需要獲得所有Segment的鎖,然後進行統計。由於Java 8拋棄了Segment,顯然是不能再這樣做了,而且這種方法雖然簡單準確但也捨棄了性能。

Java 8聲明瞭一個volatile變量baseCount用於記錄元素的個數,對這個變量的修改操作是基於CAS的,每當插入元素或刪除元素時都會調用addCount()函數進行計數。ConcurrentHashMap的計數設計與LongAdder類似。在一個低併發的情況下,就只是簡單地使用CAS操作來對baseCount進行更新,但只要這個CAS操作失敗一次,就代表有多個線程正在競爭,那麼就轉而使用CounterCell數組進行計數,數組內的每個ConuterCell都是一個獨立的計數單元。

參考文獻

HashMap最小樹形化閾值MIN_TREEIFY_CAPACITY
Java集合:HashMap詳解(JDK 1.8)
Map 大家族的那點事兒 ( 7 ) :ConcurrentHashMap ( 下 )
ConcurrentHashMap源碼導讀之initTable()方法
ConcurrentHashMap 1.7和1.8區別【這個寫的很好】
ConcurrentHashMap 1.8爲什麼要使用CAS+Synchronized取代Segment+ReentrantLock

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