ConcurrentHashmap的鎖是如何加的?是不是分段越多越好

JDK1.8的ConcurrentHashmap

利用 ==CAS + synchronized== 來保證併發更新的安全 
底層使用==數組+鏈表+紅黑樹==來實現

---------------------------------------------------------------------------------------

線程不安全的HashMap

因爲多線程環境下,使用Hashmap進行put操作會引起死循環,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。

效率低下的HashTable容器

     HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。因爲當一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如線程1使用put進行添加元素,線程2不但不能使用put方法添加元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。

ConcurrentHashMap的鎖分段技術

     HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因爲所有訪問HashTable的線程都必須競爭同一把鎖,那假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。

 

---------------------------------------------------------------------------------

Java的ConcurrentHashMap簡介

簡介

ConcurrentHashMap是Java中的一個線程安全且高效的HashMap實現。平時涉及高併發如果要用map結構,那第一時間想到的就是它。

那麼我就這幾個方面瞭解一下ConcurrentHashMap:

1)ConcurrentHashMap在JDK8裏結構
2)ConcurrentHashMap的put方法、szie方法等
3)ConcurrentHashMap的擴容
4)HashMap、Hashtable、ConccurentHashMap三者的區別
5)ConcurrentHashMap在JDK7和JDK8的區別

源碼分析

ConcurrentHashMap在JDK8裏結構

首先來看下底層的組成結構:

其實和 1.8 HashMap 結構類似,當鏈表節點數超過指定閾值的話,也是會轉換成紅黑樹的,大體結構也是一樣的。

那麼它到底是如何實現線程安全的?
答案:其中拋棄了原有的Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。至於如何實現,那我繼續看一下put方法邏輯

put方法的邏輯

ConcurrentHashMap最常用的方法也就是put方法和get方法,那麼下面主要看代碼註釋,便於理解。

 

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //1. 計算key的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //2. 如果當前table還沒有初始化先調用initTable方法將tab進行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //3. tab中索引爲i的位置的元素爲null,則直接使用CAS將值插入即可
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //4. 當前正在擴容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //5. 當前爲鏈表,在鏈表中插入新的鍵值對
                    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;
                                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;
                            }
                        }
                    }
                    // 6.當前爲紅黑樹,將新的鍵值對插入到紅黑樹中
                    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;
                        }
                    }
                }
            }
            // 7.插入完鍵值對後再根據實際大小看是否需要轉換成紅黑樹
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //8.對當前容量大小進行檢查,如果超過了臨界值(實際大小*加載因子)就需要擴容 
    addCount(1L, binCount);
    return null;
}

請先看完代碼註釋,有個大致的瞭解,然後我們更加詳細的學習一下:

這個put的過程很清晰,對當前的table進行無條件自循環直到put成功,可以分成以下六步流程來概述:

1、判斷Node[]數組是否初始化,沒有則進行初始化操作
2、通過hash定位數組的索引座標,是否有Node節點,如果沒有則使用CAS進行添加(鏈表的頭節點),添加失敗則進入下次循環。
3、檢查到內部正在擴容,就幫助它一塊擴容。
4、如果f!=null,則使用synchronized鎖住f元素(鏈表/紅黑樹的頭元素)。如果是Node(鏈表結構)則執行鏈表的添加操作;如果是TreeNode(樹型結構)則執行樹添加操作。
5、判斷鏈表長度已經達到臨界值8(默認值),當節點超過這個值就需要把鏈表轉換爲樹結構
6、如果添加成功就調用addCount()方法統計size,並且檢查是否需要擴容

1.spread(key,hashCode())

這方法作用重哈希,以減小Hash衝突

 

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

該方法主要是將key的hashCode的低16位於高16位進行異或運算,這樣不僅能夠使得hash值能夠分散能夠均勻減小hash衝突的概率,另外只用到了異或運算,在性能開銷上也能兼顧。

2.initTable方法

主要作用將tab進行初始化

 

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            // 1. 保證只有一個線程正在進行初始化操作
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // 2. 得出數組的大小
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    // 3. 這裏才真正的初始化數組
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 4. 計算數組中可用的大小:實際大小n*0.75(加載因子)
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

爲了保證能夠正確初始化,在第1步中會先通過if進行判斷,若當前已經有一個線程正在初始化即sizeCtl值變爲-1,這個時候其他線程在If判斷爲true從而調用Thread.yield()讓出CPU時間片。正在進行初始化的線程會調用U.compareAndSwapInt方法將sizeCtl改爲-1即正在初始化的狀態。另外還需要注意的事情是,在第四步中會進一步計算數組中可用的大小即爲數組實際大小n乘以加載因子0.75.可以看看這裏乘以0.75是怎麼算的,0.75爲四分之三,這裏n - (n >>> 2)是不是剛好是n-(1/4)n=(3/4)n,挺有意思的吧:)。如果選擇是無參的構造器的話,這裏在new Node數組的時候會使用默認大小爲DEFAULT_CAPACITY(16),然後乘以加載因子0.75爲12,也就是說數組的可用大小爲12。

3.CAS關鍵操作

tabAt()該方法用來獲取table數組中索引爲i的Node元素
casTabAt()利用CAS操作設置table數組中索引爲i的元素
setTabAt()該方法用來設置table數組中索引爲i的元素

4.ConcurrentHashMap的擴容

通過判斷該節點的hash值是不是等於-1(MOVED),代碼爲(fh = f.hash) == MOVED,說明 Map 正在擴容。那麼就幫助 Map 進行擴容。以加快速度。
如何幫助擴容呢?那要看看 helpTransfer 方法的實現。

 

/**
 * Helps transfer if a resize is in progress.
 */
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 如果 table 不是空 且 node 節點是轉移類型,數據檢驗
    // 且 node 節點的 nextTable(新 table) 不是空,同樣也是數據校驗
    // 嘗試幫助擴容
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 根據 length 得到一個標識符號
        int rs = resizeStamp(tab.length);
        // 如果 nextTab 沒有被併發修改 且 tab 也沒有被併發修改
        // 且 sizeCtl  < 0 (說明還在擴容)
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            // 如果 sizeCtl 無符號右移  16 不等於 rs ( sc前 16 位如果不等於標識符,則標識符變化了)
            // 或者 sizeCtl == rs + 1  (擴容結束了,不再有線程進行擴容)(默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減一。這個時候,sc 就等於 rs + 1)
            // 或者 sizeCtl == rs + 65535  (如果達到最大幫助線程的數量,即 65535)
            // 或者轉移下標正在調整 (擴容結束)
            // 結束循環,返回 table
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 如果以上都不是, 將 sizeCtl + 1, (表示增加了一個線程幫助其擴容)
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 進行轉移
                transfer(tab, nextTab);
                // 結束循環
                break;
            }
        }
        return nextTab;
    }
    return table;
}

基本邏輯已在代碼註釋中,這裏關鍵transfer(),那麼我們繼續深入瞭解一下

 

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // 每核處理的量小於16,則強制賦值16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];        //構建一個nextTable對象,其容量爲原來容量的兩倍
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // 連接點指針,用於標誌位(fwd的hash值爲-1,fwd.nextTable=nextTab)
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        // 當advance == true時,表明該節點已經處理過了
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // 控制 --i ,遍歷原hash表中的節點
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // 用CAS計算得到的transferIndex
                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;        // table 指向nextTable
                    sizeCtl = (n << 1) - (n >>> 1);     // sizeCtl閾值爲原來的1.5倍
                    return;     // 跳出死循環,
                }
                // CAS 更擴容閾值,在這裏面sizectl值減一,說明新加入一個線程參與到擴容操作
                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
                }
            }
            // 遍歷的節點爲null,則放入到ForwardingNode 指針節點
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // f.hash == -1 表示遍歷到了ForwardingNode節點,意味着該節點已經處理過了
            // 這裏是控制併發擴容的核心
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 節點加鎖
                synchronized (f) {
                    // 節點複製工作
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        // fh >= 0 ,表示爲鏈表節點
                        if (fh >= 0) {
                            // 構造兩個鏈表  一個是原鏈表  另一個是原鏈表的反序排列
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & 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) {
                                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);
                            }
                            // 在nextTable i 位置處插上鍊表
                            setTabAt(nextTab, i, ln);
                            // 在nextTable i + n 位置處插上鍊表
                            setTabAt(nextTab, i + n, hn);
                            // 在table i 位置處插上ForwardingNode 表示該節點已經處理過了
                            setTabAt(tab, i, fwd);
                            // advance = true 可以執行--i動作,遍歷節點
                            advance = true;
                        }
                        // 如果是TreeBin,則按照紅黑樹進行處理,處理邏輯與上面一致
                        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;
                        }
                    }
                }
            }
        }
    }

擴容過程有點複雜,可以查看上面註釋。這裏主要涉及到多線程併發擴容,ForwardingNode的作用就是支持擴容操作,將已處理的節點和空節點置爲ForwardingNode,併發處理時多個線程經過ForwardingNode就表示已經遍歷了,就往後遍歷,下圖是多線程合作擴容的過程:

5.treeifyBin()鏈表轉紅黑樹的過程

基本邏輯都在代碼註釋中。

 

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        //如果整個table的數量小於64,就擴容至原來的一倍,不轉紅黑樹了
        //因爲這個閾值擴容可以減少hash衝突,不必要去轉紅黑樹
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        //封裝成TreeNode
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    //通過TreeBin對象對TreeNode轉換成紅黑樹
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

addCount()方法計算ConcurrentHashMap的size

 

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //更新baseCount,table的數量,counterCells表示元素個數的變化
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        //如果多個線程都在執行,則CAS失敗,執行fullAddCount,全部加入count
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
     //check>=0表示需要進行擴容操作
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            //當前線程發起庫哦哦讓操作,nextTable=null
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

put的流程現在已經分析完了,你可以從中發現,他在併發處理中使用的是樂觀鎖,當有衝突的時候才進行併發處理,而且流程步驟很清晰,但是細節設計的很複雜,畢竟多線程的場景也複雜.

get方法

concurrentHashMap的get操作的流程很簡單,可以分爲三個步驟來描述:

1.計算hash值,定位到該table索引位置,如果是首節點符合就返回。
2.如果遇到擴容的時候,會調用標誌正在擴容節點ForwardingNode的find方法,查找該節點,匹配就返回。
3.以上都不符合的話,就往下遍歷節點,匹配就返回,否則最後就返回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()); //計算兩次hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {//讀取首節點的Node元素
        if ((eh = e.hash) == h) { //如果該節點就是首節點就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值爲負值表示正在擴容,這個時候查的是ForwardingNode的find方法來定位到nextTable來
        //查找,查找到就返回
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//既不是首節點也不是ForwardingNode,那就往下遍歷
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

szie方法的邏輯

對於size的計算,在擴容和addCount()方法就已經有處理了,可以注意一下Put函數,裏面就有addCount()函數,早就計算好的,然後你size的時候直接給你.

 

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a; //變化的數量
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

HashMap、Hashtable、ConccurentHashMap三者的區別

HashMap線程不安全,數組+鏈表+紅黑樹
Hashtable線程安全,鎖住整個對象,數組+鏈表
ConccurentHashMap線程安全,CAS+同步鎖,數組+鏈表+紅黑樹
HashMap的key,value均可爲null,其他兩個不行。

在JDK1.7和JDK1.8中的區別

在JDK1.8主要設計上的改進有以下幾點:

1、不採用segment而採用node,鎖住node來實現減小鎖粒度
2、設計了MOVED狀態 當resize的中過程中 線程2還在put數據,線程2會幫助resize。
3、使用3個CAS操作來確保node的一些操作的原子性,這種方式代替了鎖。
4、sizeCtl的不同值來代表不同含義,起到了控制的作用。
採用synchronized而不是ReentrantLock

參考文章

https://www.cnblogs.com/aspirant/p/8623864.html
https://juejin.im/post/5aeeaba8f265da0b9d781d16

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