四千字從源碼分析ConcurrentHashMap的底層原理(JDK1.8)

Map中用到最多的是HashMap,有關HashMap的介紹和底層源碼的分析可以看我之前的文章。

java集合深入理解(五):HashMap、HashTable、TreeMap的底層源碼分析和對比

HashMap有個很致命的問題就是他並非線程安全,因此在多線程環境下使用HashMap會出現問題,HashTable線程安全,但是它的效率太低了,ConcurrentHashMap就出現了,ConcurrentHashMap兼顧了線程安全和速度,下面就從底層源碼出發來了解一下ConcurrentHashMap。這裏用到的JDK版本是1.8。

目錄

1.ConcurrentHashMap概述

2.ConcurrentHashMap的使用

3.ConcurrentHashMap的原理解析

4.ConcurrentHashMap初始化

5.put操作

6.擴容操作

7.get操作

8.remove操作

9.總結


1.ConcurrentHashMap概述

首先看一下官方對ConcurrentHashMap的介紹,這段介紹來自Java Platform SE8

A hash table supporting full concurrency of retrievals and high expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access.

英文好的同學直接看上面的英語介紹,下面是我蹩腳的翻譯

一個支持檢索時完全併發性和更新時高併發性的哈希表。這個類遵循與Hashtable相同的函數規範,幷包含與每個Hashtable方法對應的方法版本。但是,即使所有操作都是線程安全的,檢索操作也不需要加鎖,並且不支持以阻止所有訪問的方式鎖定整個表。

簡單來講,和HashTable相比,ConcurrentHashMap效率更高並且不會對整張表進行加鎖,檢索時也不需要加鎖。

2.ConcurrentHashMap的使用

ConcurrentHashMap使用不難,注意ConcurrentHashMap傳入的key和value不能爲空,put操作爲key和value均添加了@NotNull註解

 ConcurrentHashMap的使用和其他的Map類集合相同,用的比較多的如put、get、remove。下面展示這些常見的用法:

ConcurrentHashMap cchashMap = new ConcurrentHashMap();
//put添加數據
cchashMap.put("1","java");
cchashMap.put("2","C");
cchashMap.put("3","C++");

System.out.println(cchashMap.get("1")); //java
System.out.println(cchashMap.size()); //3
cchashMap.remove("1");
System.out.println(cchashMap.size()); //2
//其中一種遍歷方式
Iterator<Map.Entry<String,String>> iterator=cchashMap.entrySet().iterator();
while (iterator.hasNext()){
    Map.Entry<String,String> entry=iterator.next();
    System.out.println(entry.getKey()+":"+entry.getValue());
}

3.ConcurrentHashMap的原理解析

ConcurrentHashMap做到了線程安全,其併發性通過CAS+synchronized鎖來實現

ConcurrentHashMap底層和Hashmap一樣通過數組+鏈表+紅黑樹的方式實現。

JDK1.8中的ConcurrentHashMap數據結構如下所示:

Node是ConcurrentHashMap中存放key、value以及key的hash值的數據結構:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    //具體內部方法參照源碼
}

當鏈表轉化成紅黑樹時,用TreeNode存儲對象

static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    //具體方法見源碼內部
}

在數組中,轉變爲紅黑樹後存放的不是TreeNode對象,而是TreeBin對象

static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;
    volatile Thread waiter;
    volatile int lockState;
    // values for lockState
    static final int WRITER = 1; // set while holding write lock
    static final int WAITER = 2; // set when waiting for write lock
    static final int READER = 4; // increment value for setting read lock
    //具體方法見源碼內部
}

4.ConcurrentHashMap初始化

ConcurrentHashMap提供了五種構造方法:

//無參構造方法,創建一個concurrenthashmap對象
public ConcurrentHashMap() {
}
//傳入初始容量的參數,如果傳入的值非2的冪次方,tableSizeFor會將值修改爲2的冪次方
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;
}
//傳入一個map集合,執行put操作
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}
//傳入初試容量與負載因子後執行最後一個構造方法
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}
//修改初始值和負載因子
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

ConcurrentHashMap的構造方法都沒有實際對table進行初始化對table的初始化會放在put時

下面是初始化的代碼,在初始化table中,就體現出了線程安全的一些操作,比如第六行代碼使用CAS操作來控制只能有一個線程初始化table。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) { //如果表爲空則執行初始化操作
        if ((sc = sizeCtl) < 0)  //如果sizeCtl小於0,說明此時有其他線程在初始化或擴展表
            Thread.yield(); // 使當前線程由執行狀態,變成爲就緒狀態,讓出cpu時間
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //通過cas操作去競爭初始化表的操作,設定爲-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;  //sizeCtl長度爲數組長度的3/4
            }
            break;
        }
    }
    return tab;
}

代碼中sizeCtl是用來控制table初始化和擴容的,初始化時制定了大小,爲數組的3/4。當其爲負值時,表示表正在初始化或擴容。-1表示初始化,-(1+n)表示幾個線程正在擴容

5.put操作

調用put方法後會跳轉到putVal方法中執行其中的代碼,簡單來講:第一次添加元素時,默認容量爲16,當table爲空時,直接將元素放在table上,如果不爲空,則通過鏈表或紅黑樹的方式存放。鏈表轉紅黑樹的條件爲:鏈表長度大於等於8,並且table容量大於等於64。詳細過程我已經在代碼中註釋出來。

public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException(); //判斷key和value是否爲空
    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) //第一次put時進行初始化
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //通過&運算計算這個key在table中的位置
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // 如果該位置沒有元素,通過cas操作添加元素,此時沒有上鎖
        else if ((fh = f.hash) == MOVED)  //如果檢測到hash值時MOVED,表示正在進行數組擴張的數據複製階段
            tab = helpTransfer(tab, f);  //執行helpTransfer方法幫助複製,減少性能損失
        else {
            /*
            *如果這個位置有元素就進行加鎖,
            *如果是鏈表,就遍歷所有元素,如果存在相同key,則覆蓋value,否則將數據添加在尾部
            *如果是紅黑樹,則調用putTreeVal的方式添加元素
            *最後判斷同一節點鏈表元素個數是否達到8個,達到就轉鏈表爲紅黑樹或擴容
            */
            V oldVal = null;
            synchronized (f) {//加鎖
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) { //遍歷鏈表,存在相同key則覆蓋,否則添加元素到尾部
                            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) { //如果是紅黑樹,則調用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) //當一個節點中元素數量大於等於8的時候,執行treeifyBin
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

6.擴容操作

在上一段代碼中我們可以看到,當一條鏈表中元素個數大於等於8時,會執行treeifyBin來判斷是擴容還是轉化爲紅黑樹。

/*
*當table長度小於64的時候,擴張數組長度一倍,否則把鏈表轉化爲紅黑樹
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //如果table長度小於64
            tryPresize(n << 1);  //table長度擴大一倍
        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<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;
                    }
                    setTabAt(tab, index, new TreeBin<K,V>(hd)); //把頭節點放入容器TreeBin中
                }
            }
        }
    }
}

再來看一下擴容的操作,擴容操作傳入的參數是size,會通過size計算出一個c值,然後用c值和sizeCtl進行比較,直到sizeCtl大於等於c時,纔會停止擴容。

private final void tryPresize(int size) {
    //計算c的大小,如果size比最大容量一半還大,則直接等於最大容量,否則通過tableSizeFor計算出一個2的冪次方的數
    //計算出的這個c會與sizeCtl進行比較,一直到sizeCtl>=c時纔會停止擴容
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    //另sc等於sizeCtl
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        //如果table爲空則初始化,這裏和初始化時代碼一樣
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        //如果c比sizeCtl要小或者table的長度大於最大長度才停止擴容
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {
            int rs = resizeStamp(n);
            //如果正在擴容(sc<0),幫助擴容
            if (sc < 0) {
                Node<K,V>[] nt;
                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);
            }
            //否則直接進行擴容
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

到這裏put操作就算是結束了。

7.get操作

看完put後後面的操作就簡單了,get操作不設計線程安全,因此不用加鎖。首先通過hash值判斷該元素放在table的哪個位置,通過遍歷的方式找到指定key的值,不存在返回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()); //計算key的hash值
    //如果table不爲空並且table的容量大於0並且key在table的位置不等於空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) { 
            //如果table上的key就是要找的key,返回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;
}

8.remove操作

調用remove方法後會自動跳轉到replaceNode方法中,刪除節點的主要過程爲首先判斷table是否爲空,再判斷是否正在擴容,通過遍歷的方式找到節點後刪除。通過對單個鏈表或紅黑樹加鎖的方式使得可以多線程刪除元素。

public V remove(Object key) {
    return replaceNode(key, null, null);
}

final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //如果table爲空或者發現不存在該key,直接退出循環
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
         //如果等於MOVED,表示其他線程正在擴容,幫助擴容   
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            synchronized (f) {
                //二次校驗,如果tabAt(tab, i)不等於f,說明已經被修改了
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        validated = true;
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            //找到對應的節點
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                //刪除節點或者更新節點的條件
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    //更新節點
                                    if (value != null)
                                        e.val = value;
                                    //刪除非頭節點    
                                    else if (pred != null)
                                        pred.next = e.next;
                                    //刪除頭節點    
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            //繼續遍歷
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    //如果是紅黑樹則按照樹的方式刪除或更新節點
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            if (validated) {
                if (oldVal != null) {
                    //如果刪除了節點,更新長度
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

9.總結

ConcurrentHashmap通過cas和synchronized鎖的方式實現了線程安全,通過一個Node<K,V>數組保存map鍵值對,在通過數組下通過鏈表和紅黑樹保存元素。第一次調用構造方法時不會初始化table,初始化table會在put操作時初始化。

因爲可以讓多個線程同時處理,在ConcurrentHashmap中增加了一個sizeCtl變量,這個變量用來控制table的初始化和擴容,

sizeCtl :默認爲0,用來控制table的初始化和擴容操作

-1 代表table正在初始化

-N 取-N對應的二進制的低16位數值爲M,此時有M-1個線程進行擴容

其餘情況:

1、如果table未初始化,表示table需要初始化的大小。

2、如果table初始化完成,表示table的容量,默認是table大小的0.75倍

第一次添加元素時,默認容量爲16,當table爲空時,直接將元素放在table上,如果不爲空,則通過鏈表或紅黑樹的方式存放。鏈表轉紅黑樹的條件爲:鏈表長度大於等於8,並且table容量大於等於64。

以上是我對ConcurrentHashmap底層源碼的總結,如果有任何問題可以在評論區反饋或者私信我,我都會回。歡迎關注我的同名微信公衆號,目前處於起步階段,大家一起學習一起進步!

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