哈希表和 Java 的前世今生(完),掌握HashMap看這一篇就夠了!!!

哈希表和 Java 的前世今生(上),掌握HashMap看這一篇就夠了!!! 中,我們講解了哈希表的原理以及JDK7 HashMap的源碼及JDK7中HashMap的注意點。
哈希表和 Java 的前世今生(中),掌握HashMap看這一篇就夠了!!!中,我們講解了JDK8 HashMap的源碼,以及其與JDK7 HashMap的區別
哈希表和 Java 的前世今生(下),掌握HashMap看這一篇就夠了!!!中 我們講解了 Hashtable與HashMap的區別以及JDK7 ConcurrentHashMap的原理以及源碼解讀
今天是本次HashMap的最後一篇講解,主要講解JDK8 ConcurrentHashMap的原理以及源碼解讀

七、JDK8 中 ConcurrentHashMap

7.1 問題 13:JDK8 中 ConcurrentHashMap 變化

  1. 結構簡單:JDK8 拋棄 JDK7 的 Segment 分段鎖機制,由 JDK7 的兩級數組變回了原來的一級數組。鏈表長度>=8,該鏈表轉換爲紅黑樹。
    在這裏插入圖片描述

  2. 降低鎖的粒度:鎖住數組的每個桶的頭結點,鎖粒度更小。(Hashtable 是鎖住整個表、JDK7 的 ConcurrrentHashMap 是鎖住一個段 Segment。而這裏是鎖住一個鏈表或者一個紅黑樹)

  3. 鎖變化:不使用 Segment 鎖(繼承 ReentrantLock),利用 CAS+Synchronized來保證併發安全。

  4. 併發擴容,多個線程參與。(JDK7 的 ConncurrentHashMap 的 Segement 數組長度固定不擴容,擴容的每個 HashEntry 數組的容量,此時不需要考慮併發,因爲到這裏的時候,是持有該 Segment 的獨佔鎖的)

注意:JDK8 中,Segment 類依舊存在,但只是爲了兼容,只有在序列化和反序列化時纔會被用到

  1. 更多的 Node 類型
    在這裏插入圖片描述

a. Node<K,V>:基本結點/普通節點。當 table 中的 Entry 以鏈表形式存儲時才使用,存儲實際數據。該類的 key 和 value 不爲 null(其子類可爲 null)
b. TreeNode:紅黑樹結點。當 table 中的 Entry 以紅黑樹的形式存儲時纔會使用,存儲實際數據。ConcurrentHashMap 中對 TreeNode 結點的操作都會由 TreeBin 代理執行。
c. TreeBin:代理操作 TreeNode 結點。該節點的 hash 值固定爲-2,存儲實際數據的紅黑樹的根節點。因爲紅黑樹進行寫入操作整個樹的結構可能發生很大變化,會影響到讀線程。因此 TreeBin 需要維護一個簡單的讀寫鎖,不用考慮寫-寫競爭的情況。當然並不是全部的寫操作都需要加寫鎖,只有部分put/remove 需要加寫鎖。
d. ForwardingNode:轉發結點。該節點是一種臨時結點,只有在擴容進行中才會出現,該節點的 hash 值固定爲-1,並且它不存儲實際數據。如果舊 table的一個 hash 桶中全部結點都遷移到新的數組中,舊 table 就在桶中放置一個ForwardingNode。當讀操作或者迭代操作遇到 ForwardingNode 時,將操作轉發到擴容後新的 table 數組中去執行,當寫操作遇見 ForwardingNode時,則嘗試幫助擴容。
在這裏插入圖片描述

e. ReservationNode:保留結點,也被稱爲空節點。該節點的 hash 值固定爲-3,不保存實際數據。正常的寫操作都需要對 hash 桶的第一個節點進行加鎖,如果 hash 桶的第一個節點爲 null 時是無法加鎖的,因此需要 new 一個ReservationNode 節點,作爲 hash 桶的第一個節點,對該節點進行加鎖。

7.2 問題 14:JDK8 ConcurrentHashMap 怎麼放棄 Lock 使用 synchronized 了

  1. synchronized 之前一直都是重量級鎖,但是 JDK6 中官方是對他進行過升級,引入了偏向鎖,輕量級鎖,重量級鎖,現在採用的是鎖升級的方式去做的。針對synchronized 獲取鎖的方式,JVM 使用了鎖升級的優化方式,就是先使用偏向鎖優先同一線程然後再次獲取鎖,如果失敗,就升級爲 CAS 輕量級鎖,如果失敗就會短暫自旋,防止線程被系統掛起。最後如果以上都失敗就升級爲重量級鎖。所以是一步步升級上去的,最初也是通過很多輕量級的方式鎖定的。

  2. ReentantLock 是 JDK 層面的,synchronized 是 JVM 層面的。相對而言,synchronized 的性能優化空間更大,這就使得 synchronized 能夠隨着 JDK 版本的升級而不改動代碼的前提下獲得性能上的提升。

  3. 另外此處 synchronized 鎖住的是單個鏈表的頭結點,粒度小,而不是 Hashtable、Collections 等鎖整個哈希表。低粒度下,synchronized 和 Lock 的差異沒有高粒度下明顯。

對象頭 Mark Word(標記字段)

在這裏插入圖片描述

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度 如果始終得不到鎖,競爭的線程使用自旋會消耗 CPU 追求響應時間,鎖佔用時間很短
重量級鎖 線程競爭不使用自旋,不會消耗 CPU 線程阻塞,響應時間緩慢 追求吞吐量,鎖佔用時間較長

7.3 問題 15:sizeCtl 屬性的作用

sizeCtl 屬性是 ConcurrentHashMap 中出鏡率很高的一個屬性,因爲它是一個控制標識符,在不同的地方有不同用途,而且它的取值不同,也代表不同的含義。

  • -1 代表正在初始化
  • -N 表示有 N-1 個線程正在進行擴容操作
  • 正數或 0 代表 hash 表還沒有被初始化,這個數值表示初始化或下一次進行擴容的大小,這一點類似於擴容閾值的概念。還後面可以看到,它的值始終是當前ConcurrentHashMap 容量的 0.75 倍,這與 loadfactor 是對應的。

7.4 問題 16:折騰什麼?Hashtable 不可存儲 null key-value,HashMap可以,ConcurrentHashMap 不可

其實你發現 Hashtable、ConcurrentHashMap 不允許 null,但是都是線程安全的,適用於多線程環境。而 HashMap 卻是線程不安全的,適用於單線程環境。和此有關嗎?其實有關係的。

無法容忍的歧義。如果 map.get(key) return null,是 key 不存在呢,還是 value 是null 呢?單線程情況下可以區分,可以通過先調用 map.contains(key)來辨別,但在並行映射中,兩次調用 map.contains(key)和 map.get(key) 之間,映射可能已更改。

7.5 JDK8 ConcurrentHashMap 源碼閱讀:put( )

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 均不能爲空
        if (key == null || value == null) throw new NullPointerException();
// 得到 hash 值
        int hash = spread(key.hashCode());
// 用於記錄相應鏈表的長度
        int binCount = 0;
        for (Node<K, V>[] tab = table; ; ) {
            Node<K, V> f;
            int n, i, fh;
// 如果數組"空",進行數組初始化
            if (tab == null || (n = tab.length) == 0)
// 初始化數組,後面會詳細介紹
                tab = initTable();
// 找該 hash 值對應的數組下標,得到第一個節點 f
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果數組該位置爲空,用一次 CAS 操作將這個新值放入其中即可,
//這個 put 操作差不多就結束了,可以拉到最後面了
// 如果 CAS 失敗,那就是有併發操作,進到下一個循環就好了
                if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
                    break; // no lock when adding to empty bin
                     }
// hash 等於 MOVED,表示正在擴容
                else if ((fh = f.hash) == MOVED)
// 幫助數據遷移
                    tab = helpTransfer(tab, f);
                else { // 到這裏就是說,f 是該位置的頭結點,而且不爲空
                    V oldVal = null;
// 獲取數組該位置的頭結點的監視器鎖
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) { // 頭結點的 hash 值大於 0,說明是鏈表
// 用於累加,記錄鏈表的長度
                                binCount = 1;
// 遍歷鏈表
                                for (Node<K, V> e = f; ; ++binCount) {
                                    K ek;
// 如果發現了"相等"的 key,進行值覆蓋,
                                    if (e.hash == hash &&
                                            ((ek = e.key) == key ||
                                                    (ek != null && key.equals(ek)))) {
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
// 到了鏈表的最末端,將這個新值放到鏈表的最後面
                                    Node<K, V> pred = e;
                                    if ((e = e.next) == null) {
                                        pred.next = new Node<K, V>(hash, key, value, null);
                                        break;
                                    }
                                }
                            } else if (f instanceof TreeBin) { // 紅黑樹
                                Node<K, V> p;
                                binCount = 2;
// 調用紅黑樹的插值方法插入新節點
                                if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key,value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
// binCount != 0 說明上面在做鏈表操作
                    if (binCount != 0) {
// 判斷是否要將鏈表轉換爲紅黑樹,臨界值和 HashMap 一樣,也是 8
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
//節點數增加了 1 個
            addCount(1L, binCount);
            return null;
        }

八、最後的總結和交流

8.1 三代 HashMap 代碼行數的變化

在這裏插入圖片描述

編程不識 Doug Lea,寫盡 Java 也枉然。整個 JUC(java.util.concurrent)就是他的傑作。
在這裏插入圖片描述

在這裏插入圖片描述

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