Java併發集合類ConcurrentHashMap底層核心原始碼解析

頭髮少少少。發表於2021-06-15

一、概述

ConcurrentHashMap是java併發包下一個常用的併發集合類,在面試中經常會被問及,一般在講述了執行緒不安全的HashMap之後,面試官會問這個。

在這篇文章中,我會詳細分析ConcurrentHashMap中幾個重要API的底層原始碼和實現機制

本文將以JDK1.8版本進行講解

 

二、JDK1.7與1.8區別

1.8中去除了Segment+HashEntry+Unsafe的實現方式,改為Synchronized+CAS+Node+Unsafe的實現方式

Segment分段

ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列,同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

Segment內部資料結構

併發程式設計系列:ConcurrentHashMap的實現原理(JDK1.7和JDK1.8)

ConcurrentHashMap(1.7)使用了分段鎖的機制,將資料分成一段一段進行儲存,每個Segment對應一把鎖,當一個執行緒佔用鎖訪問一個Segment時,其他執行緒可以訪問其他的Segment,這就實現了真正意義上的併發。

 

在1.7版本中,定位到一個元素需要通過兩次Hash計算,第一次計算定位到Segment,第二次計算定位到HashEntry(所在元素的頭結點)

 

優缺點

優點:

在寫操作的時候只需要對當前的Segment加鎖即可,因此理想狀態下,ConcurrentHashMap同時可支援Segment數量的併發寫操作,而且不會影響到其他的Segment部分,通過這種資料結構可以大大提高併發力度。

缺點:

由於是通過Segment -》HashEntry的定位方式,需要兩次計算,相比1.8中效率比較低下

 

 

三、核心原始碼分析

 

1.ConcurrentHashMap中的Node節點

	static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * 比較常見的連結串列的搜尋方法,通過對比hash值,value來判斷是否是目標元素,如果不是的話就e = e.next向下遍歷,如果到末尾為null了
        	就返回null
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

  

2.ConcurrentHashMap的初始化

	/**
     * Creates a new, empty map with the default initial table size (16).
     	如果是空構造器,預設初始長度是16
     */
    public ConcurrentHashMap() {
    }

  

	public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

如果制定了初始容量的大小,先進行小於零的驗證,判斷初始化大小是否大於最大值的一般,如果是的話就去最大時,否則就執行tableSizeFor方法,這個方法返回的是一個大於或者等於輸入的初始化容量的一個2^n的大小

3.ConcurrentHashMap的put操作

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException(); //判斷K,V是否為空
        int hash = spread(key.hashCode()); //計算key的hash值
        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(); //如果當前的table沒有初始化,就先進行初始化
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // 通過CAS自旋的方式,將元素添封裝成Node,新增到這個位置,注意這時候是沒有加鎖的
            }
            else if ((fh = f.hash) == MOVED) //判斷是否處於擴容階段,是的話就先幫助擴容
                tab = helpTransfer(tab, f);
            else { 
                V oldVal = null;
                synchronized (f) { //如果這個位置有元素,使用sunchronized鎖對當前頭節點進行加鎖
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) { //判斷是連結串列
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {//迴圈遍歷這個連結串列
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val; //如果存在這個Key,就把值進行替換
                                    if (!onlyIfAbsent) 
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {//如果遍歷到最後不存在得話,就把Key,value封裝成Node,放到尾部
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { //如果是紅黑樹形式,使用putTreeVal方法進行新增
                            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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD) //
                        treeifyBin(tab, i);//將連結串列轉化為紅黑樹
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//計數
        return null;
    }

  

4.ConcurrentHashMap的擴容機制

 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // 每個cpu至少處理16個長度的資料元素,用於控制不佔用過多cpu資源
        if (nextTab == null) {            // 如果第一個複製的目標是空的話,初始化一個table兩倍長的nexttable
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//fwd節點,用來控制併發,當一個節點為空或者已經被轉移了,就設定為fwd節點
        boolean advance = true; //判斷是否繼續向前判斷,ConcurrentHashMap複製是從n號往0開始遍歷判斷
        boolean finishing = false; // 重新掃描陣列用的,看看有沒有沒完成的
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing) //判斷已經到頭了
                    advance = false; 
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) { //已經完成了轉移
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1); //設定閾值為擴容後的0.75
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null) //把陣列中null的元素設定為fwd節點
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else { 
                synchronized (f) {//加鎖開始轉移
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) { //判斷是一個Node節點
                            int runBit = fh & n; //這裡是判斷&操作後是0還是1,因為n的值是陣列長度,是2的冪此,所以這個結果就是,如果是0就放在新表														的同一個位置,如果是1的話就放在新表原位置+n的地方
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n; //n為擴張前原資料的長度
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) { //構造兩個連結串列,分別放到原來的位置和新增加的長度的相同位置,i																								或者n+i
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) { //判斷是一個樹節點
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            //判斷複製完之後,如果節點數小於6,就轉化為連結串列
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

  

5.ConcurrentHashMap的Get操作

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode()); //計算key的hash值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) { //如果陣列位置不為空
            if ((eh = e.hash) == h) { //判斷如果是頭節點,返回value
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) { //連結串列向下遍歷
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

  

四、ConcurrentHashMap的同步機制

前面分析了下ConcurrentHashMap的原始碼,那麼,對於一個對映集合來說,ConcurrentHashMap是如果來做到併發安全,又是如何做到高效的併發的呢?

首先是讀操作,從原始碼中可以看出來,在get操作中,根本沒有使用同步機制,也沒有使用unsafe方法,所以讀操作是支援併發操作的。

那麼寫操作呢?

分析這個之前,先看看什麼情況下會引起陣列的擴容,擴容是通過transfer方法來進行的。而呼叫transfer方法的只有trePresize、helpTransfer和addCount三個方法。

這三個方法又是分別在什麼情況下進行呼叫的呢?

·tryPresize是在treeIfybin和putAll方法中呼叫,treeIfybin主要是在put新增元素完之後,判斷該陣列節點相關元素是不是已經超過8個的時候,如果超過則會呼叫這個方法來擴容陣列或者把連結串列轉為樹。

·helpTransfer是在當一個執行緒要對table中元素進行操作的時候,如果檢測到節點的HASH值為MOVED的時候,就會呼叫helpTransfer方法,在helpTransfer中再呼叫transfer方法來幫助完成陣列的擴容

·addCount是在當對陣列進行操作,使得陣列中儲存的元素個數發生了變化的時候會呼叫的方法。

  

所以引起陣列擴容的情況如下

·只有在往map中新增元素的時候,在某一個節點的數目已經超過了8個,同時陣列的長度又小於64的時候,才會觸發陣列的擴容。

·當陣列中元素達到了sizeCtl的數量的時候,則會呼叫transfer方法來進行擴容

  

那麼在擴容的時候,可以不可以對陣列進行讀寫操作呢?

事實上是可以的。當在進行陣列擴容的時候,如果當前節點還沒有被處理(也就是說還沒有設定為fwd節點),那就可以進行設定操作。

如果該節點已經被處理了,則當前執行緒也會加入到擴容的操作中去。

  

那麼,多個執行緒又是如何同步處理的呢?

在ConcurrentHashMap中,同步處理主要是通過Synchronized和unsafe兩種方式來完成的。

·在取得sizeCtl、某個位置的Node的時候,使用的都是unsafe的方法,來達到併發安全的目的

·當需要在某個位置設定節點的時候,則會通過Synchronized的同步機制來鎖定該位置的節點。

·在陣列擴容的時候,則通過處理的步長和fwd節點來達到併發安全的目的,通過設定hash值為MOVED

·當把某個位置的節點複製到擴張後的table的時候,也通過Synchronized的同步機制來保證現程安全

 

 

感謝Ouka傅的原始碼分析,https://www.cnblogs.com/zerotomax/p/8687425.html#go5,我也是從這篇部落格裡學來的

相關文章