ConcurrentHashMap源碼分析

今天來介紹大名鼎鼎的ConcurrentHashMap,衆所周知,Java.Utils.Concurrent包出現後,就立馬成爲高併發的利器,而靠一己之力把此包寫出來的Doug Lea,則更是高併發大神。此篇文章僅僅限於描述ConcurrentHashMap冰山一角,並不能對其全面剖析,若是有讀者想要對併發進行更深刻的理解與交流,推薦《Java併發編程的藝術》,筆者看完頗有感悟。node

此文仍是從最簡單也是最經常使用的get,put方法來進行剖析,進而逐步抽絲剝繭,分析此類的全貌。理解此文章須要讀者有HashMap的基礎,且建議讀者在閱讀此文章時,腦中必定要兩個甚至多個線程的概念,切勿以單線程模型來思考。 從註釋能夠得知全部參數都不能夠爲null. 與HashMap不一樣數據庫

All arguments to all task methods must be non-null
複製代碼

ConcurrentHashMap.put()

/**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }
複製代碼

能夠看到put函數只是簡單調用了putVal這個函數,那麼繼續往下編程

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();//參數檢測異常
        int hash = spread(key.hashCode()); //Rehash
        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) {//1 此位置上尚未元素插入,則利用cas鎖,
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//2 
                tab = helpTransfer(tab, f);
            else { // 3 
                V oldVal = null;
                synchronized (f) {//3.1
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {//3.2
                                K ek;
                                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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

複製代碼

由上圖源碼可知,死循環+四個if else就是整個put函數的核心實現。四個if else分別對應以下功能數組

  • 若是table爲空,則初始化table(能夠理解爲一個數組,其實此處初始化也別有洞天,由於要防止多個線程同時初始化,有興趣的讀者能夠本身去研究一下,看看Doug Lea是用了什麼方式,防止table被屢次初始化)安全

  • 註釋1處的,若是數組對應的索引位置處尚未元素,則利用casTabAt進行放置key,value 此處主要有兩點須要注意: 1 tabAt是利用的unSafe類裏的getObjectVolatile(),熟悉Volatile關鍵字的同窗確定知道,這是獲取該對象在內存中最新的值。(即同時有多個線程修改此變量,則JVM happen-before原則可以幫助咱們獲取到最新的值)。該對象便是table對應索引處的node對象。bash

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
複製代碼

2 若是casTabAt成功,則put成功,break掉死循環。 casTabAt()也是利用了UnSafe類裏的compareAndSwapObject函數,關於此類能夠多說幾句,此類是Java用來利用CAS鎖機制而現實的一個接口類。(各位同窗必定要弄明白,CAS鎖其實不須要上層作任何操做,它的可靠性是由底層硬件指令來保證的,上層只是調用),返回一個boolean。即若是插入失敗,則重試。爲何會插入失敗,各位能夠思考一下。併發

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }


    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }
複製代碼
  • 3 註釋2處,此處判斷是否正處於轉移中,若是要插入的位置,正在轉移,也就是整個table正處於擴容階段,則幫助其轉移。app

  • 4 註釋3處,Put函數的核心。若是走到此分支,則證實函數

    • table已然初始化過。
    • table對應索引位置上已經有元素
    • table並無擴容或者轉移 因此,此時,咱們能夠進行插入,那如何確保插入操做的原子性呢?注意3.1處的synchronized 關鍵字,將f做爲鎖,保證原子性。下面又進行了一次判斷,爲何在此處須要判斷呢?你們想一想單例模式的DCL 應該就能懂了。繼續往下看,裏面又有三個if else的判斷,此處因爲篇幅關係,僅分析第一個if(fh >= 0),剩下兩個if else讀者可自行分析(相對比較簡單)。 註釋3.2處可知,又是個死循環,此處的邏輯與HashMap的大概相同,若是找到key相同的元素,則替換其Value。若是沒找到,則將其加入到鏈表的最後一位。跳出內層死循環,判斷查詢次數是否大於閾值,而後跳出外層死循環,返回。

到此整個ConcurrentHashMap的Put函數分析就結束了,是否是很簡單呢?那是由於咱們沒有分析put函數裏兩個較爲硬核的addCount()與helpTransfer()函數。高併發

總結 ConcurrentHashMap put函數的精髓就在於利用CAS替換所在位置,與鎖住鏈表表頭(或者是紅黑樹的root節點),進行修改。若是有同窗接觸過數據庫,則會聯繫到此實現相似於數據庫的行級鎖。其優勢是,下降的鎖的粒度,提升了併發的效率。其缺點 則是非絕對的線程安全。

「當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替進行,而且在主調
代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是絕對的線程安全的。」
複製代碼

接下來繼續看Get函數。

ConcurrentHashMap.get()

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code key.equals(k)},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @throws NullPointerException if the specified key is null
     */
    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)//註釋1
                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;
    }
複製代碼

額,到這裏感受沒什麼可講的,由於讀模式的話基本不涉及到線程變量衝突的問題,因此你們也能夠看到get函數跟hashmap的get函數相差並不大。 也是簡單的條件判斷,而後查詢key對應的node是否存在。 稍微有點區別的就是註釋1處了。

相關文章
相關標籤/搜索