源碼分析——ConcurrentHashMap

前言

上一篇文章我講了一下HashMap的相關源碼實現,而且咱們知道它是線程不安全的,在併發環境中使用時,HashMap在擴容的時候有可能會生成一個環形鏈表,從而致使get造成死循環超時。那這篇咱們就來介紹一下併發環境下使用的HashMap——ConcurrentHashMap,下面是它的類關係圖。java


JDK1.7中的實現

JDK1.7 中的ConcurrentHashMap採用了分段鎖的設計,先來看一下它的數據結構。ConcurrentHashMap中含有一個Segment數組。每一個Segment中又含有一個HashEntry數組。

Segment是一種可重入鎖,在ConcurrentHashMap裏扮演鎖的角色;HashEntry則用於存儲鍵值對數據。node

一個ConcurrentHashMap裏包含一個Segment數組。Segment的結構和HashMap相似,是一種數組和鏈表結構。一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每一個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先得到與它對應的Segment鎖。算法

ConcurrentHashMap經過使用分段鎖技術,將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問,可以實現真正的併發訪問。數組


來看看上述Segment結構的定義:

static final class Segment extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
    transient volatile HashEntry[] table;
    transient int count;
    transient int modCount;
    transient int threshold;
    final float loadFactor;
    ... ...
}複製代碼

1. 存儲結構

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}
複製代碼
ConcurrentHashMap 和 HashMap 實現上相似,最主要的差異是 ConcurrentHashMap 採用了分段鎖(Segment),每一個分段鎖維護着幾個桶(HashEntry),多個線程能夠同時訪問不一樣分段鎖上的桶,從而使其併發度更高(併發度就是 Segment 的個數)。
Segment 繼承自 ReentrantLock。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;

    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

    transient volatile HashEntry<K,V>[] table;

    transient int count;

    transient int modCount;

    transient int threshold;

    final float loadFactor;
}
final Segment<K,V>[] segments;複製代碼
不看下面的方法,能夠看到幾個熟悉的字段。HashEntry(哈希數組),threshold(擴容閾值),loadFactor(負載因子)表示segment是一個完整的HashMap。
接下來咱們看看ConcurrentHashMap的構造函數

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)複製代碼
三個參數分別表明了:
  • 初始容量:初始容量表示全部的segment數組中,一共含有多少個hashentry。若initialCapacity不爲2的冪,會取一個大於initialCapacity的2的冪。
  • 負載因子:默認0.75。
  • 併發級別:能夠同時容許多少個線程併發。concurrencyLevel爲多少,就有多少個segment,固然也會取一個大於等於這個值的2的冪。
默認的併發級別爲 16,也就是說默認建立 16 個 Segment。

static final int DEFAULT_CONCURRENCY_LEVEL = 16;複製代碼


接下來咱們看一下ConcurrentHashMap中的幾個關鍵函數,get,put,rehash(擴容), size方法,看看他是如何實現併發的。
安全

2. get 操做


get實現過程:
  1. 根據key,計算出hashCode;
  2. 根據步驟1計算出的hashCode定位segment,若是segment不爲null && segment.table也不爲null,跳轉到步驟3,不然,返回null,該key所對應的value不存在;
  3. 根據hashCode定位table中對應的hashEntry,遍歷hashEntry,若是key存在,返回key對應的value;
  4. 步驟3結束仍未找到key所對應的value,返回null,該key對應的value不存在。
比起Hashtable,ConcurrentHashMap的get操做高效之處在於整個get操做不須要加鎖。若是不加鎖,ConcurrentHashMap的get操做是如何作到線程安全的呢?緣由是volatile,全部的value都定義成了volatile類型,volatile能夠保證線程之間的可見性,這也是用volatile替換鎖的經典應用場景。

3. put操做

ConcurrentHashMap提供兩個方法put和putIfAbsent來完成put操做,它們之間的區別在於put方法作插入時key存在會更新key所對應的value,而putIfAbsent不會更新。


put實現過程:
  1. 參數校驗,value不能爲null,爲null時拋出空指針異常;
  2. 計算key的hashCode;
  3. 定位segment,若是segment不存在,建立新的segment;
  4. 調用segment的put方法在對應的segment作插入操做。
putIfAbsent實現過程:


putIfAbsent的執行過程與put方法是一致的,除了最後調用的segment的put方法參數onlyIfAbsent傳參不同。

segment的put方法實現bash

segment的put方法是整個put操做的核心,它實現了在segment的HashEntry數組中作插入(segment的HashEntry數組採用拉鍊法來處理衝突)。


segment put實現過程:
1. 獲取鎖,保證put操做的線程安全;
2. 定位到HashEntry數組中具體的HashEntry;
3. 遍歷HashEntry鏈表,倘若待插入key已存在:
  • 須要更新key所對應value(!onlyIfAbsent),更新oldValue=newValue,跳轉到步驟5;
  • 不然,直接跳轉到步驟5;
4. 遍歷完HashEntry鏈表,key不存在,插入HashEntry節點,oldValue=null,跳轉到步驟5;
5. 釋放鎖,返回oldValue。

步驟4在作插入的時候實際上經歷了兩個步驟:
  • 第一:HashEntry數組擴容;
是否須要擴容
在插入元素前會先判斷Segment的HashEntry數組是否超過threshold,若是超過閥值,則須要對HashEntry數組擴容;
如何擴容
在擴容的時候,首先建立一個容量是原來容量兩倍的數組,將原數組的元素再散列後插入到新的數組裏。爲了高效,ConcurrentHashMap只對某個Segment進行擴容,不會對整個容器擴容。
  • 第二:定位添加元素對應的位置,而後將其放到HashEntry數組中。

4. size 操做

每一個 Segment 維護了一個 count 變量來統計該 Segment 中的鍵值對個數。

/** * The number of elements. Accessed only either within locks * or among other volatile reads that maintain visibility. */
transient int count;複製代碼
在執行 size 操做時,須要遍歷全部 Segment 而後把 count 累計起來。
ConcurrentHashMap 在執行 size 操做時先嚐試不加鎖,若是連續兩次不加鎖操做獲得的結果一致,那麼能夠認爲這個結果是正確的。
嘗試次數使用 RETRIES_BEFORE_LOCK 定義,該值爲 2,retries 初始值爲 -1,所以嘗試次數爲 3。
若是嘗試的次數超過 3 次,就須要對每一個 Segment 加鎖。

static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            // 超過嘗試次數,則對每一個 Segment 加鎖
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            // 連續兩次獲得的結果一致,則認爲這個結果是正確的
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}複製代碼

因爲在累加count的操做的過程當中以前累加過的count發生變化的概率很是小,因此ConcurrentHashMap先嚐試2次不鎖住Segment的方式來統計每一個Segment的大小,若是在統計的過程當中Segment的count發生了變化,這時候再加鎖統計Segment的count。

ConcurrentHashMap如何判斷統計過程當中Segment的cout發生了變化?數據結構

Segment使用變量modCount來表示Segment大小是否發生變化,在put/remove/clean操做裏都會將modCount加1,那麼在統計size的先後只須要比較modCount是否發生了變化,若是發生變化,Segment的大小確定發生了變化。

JDK 1.8 的改動

JDK 1.7 使用分段鎖機制來實現併發更新操做,核心類爲 Segment,它繼承自重入鎖 ReentrantLock,併發度與 Segment 數量相等。
JDK 1.8 使用了 CAS 操做來支持更高的併發度,在 CAS 操做失敗時使用內置鎖 synchronized。
1.7中Segment[]最大是16,也就是最大支持16個併發。1.8改爲Node[],課並行的數量遠遠大於16。
而且 JDK 1.8 的實現也在鏈表過長時會轉換爲紅黑樹,這點與HashMap1.8的實現是同樣的。

數據結構採用數組 + 鏈表 + 紅黑樹的方式實現。當鏈表中(bucket)的節點個數超過8個時,會轉換成紅黑樹的數據結構存儲,這樣設計的目的是爲了提升同一個鏈表衝突過大狀況下的讀取效率。併發

Java8中主要作了以下優化:

  1. 將Segment拋棄掉了,直接採用Node(繼承自Map.Entry)做爲table元素。
  2. 修改時,再也不採用ReentrantLock加鎖,直接用內置synchronized加鎖,Java8的內置鎖比以前版本優化了不少,相較ReentrantLock,性能不併差。
  3. size方法優化,增長CounterCell內部類,用於並行計算每一個bucket的元素數量。

JDK1.8中,出現了較大的改動。沒有使用段鎖,改爲了Node數組 + 鏈表 + 紅黑樹的方式。
其中有個重要的變量:sizeCtl
  • 負數表示正在進行初始化或者擴容,-1表示正在初始化,-N表示有N - 1個線程正在擴容
  • 正數0,表示尚未被初始化。其餘正數表示下一次擴容的大小。
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;
}複製代碼
還有兩個數據結構TreeNode、TreeBin,用來當鏈表的大小超過閾值的時候,將鏈表變做紅黑樹。

CAS 操做

這一版本大量使用了CAS操做。所謂的CAS就是,比較內存對應的區域的值,和指望值是否是相等,若是相等,就設置一個新的值進去。
通常是這樣使用,先獲取對象中的某個域的值,並以這個值爲指望值去調用CAS算法。

ConcurrentHashMap中有三個核心的CAS操做async

  • tabAt:得到數組中位置i上的節點
  • casTabAt:設置數組位置i上的節點
  • setTabAt:利用volatile設置位置i上的節點。

//獲取索引i處Node
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);  
}  
//利用CAS算法設置i位置上的Node節點(將c和table[i]比較,相同則插入v)
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);  
}  
//利用volatile設置節點位置i的值,僅在上鎖區被調用 
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {  
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);  
}  複製代碼

initTable() 方法

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        //若是一個線程發現sizeCtl<0,意味着另外的線程
        //執行CAS操做成功,當前線程只須要讓出cpu時間片
        if ((sc = sizeCtl) < 0)
            Thread.yield();
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            //CAS方法把sizectl置爲-1,表示本線程正在進行初始化
            try {
                if ((tab = table) == null || tab.length == 0) {
                    //DEFAULT_CAPACITY 默認初始容量是 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    //初始化數組,長度爲 16 或初始化時提供的長度
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    //將這個數組賦值給 table,table 是 volatile 的
                    table = tab = nt;
                    //若是 n 爲 16 的話,那麼這裏 sc = 12
                    //其實就是 0.75 * n
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}複製代碼
在put方法調用時,回去判斷table是否是爲null,若爲null就調用initTable去初始化。

調用initTable會判斷sizeCtl的值,若值爲-1則表示正在初始化,會調用yield()去等待。函數

若值爲0,這時先調用CAS算法去設置爲-1,再初始化。

因此執行第一次put操做的線程會執行Unsafe.compareAndSwapInt方法修改sizeCtl爲-1,有且只有一個線程可以修改爲功,其它線程經過Thread.yield()讓出CPU時間片等待table初始化完成。

綜上所述,能夠知道初始化是單線程操做。

put()方法

假設table已經初始化完成,put操做採用CAS+synchronized實現併發插入或更新操做,具體實現以下。

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

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //不容許key、value爲空
    if (key == null || value == null) throw new NullPointerException();
    //返回 (h ^ (h >>> 16)) & HASH_BITS;
    int hash = spread(key.hashCode());
    int binCount = 0;
    //循環,直到插入成功
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            //table爲空,初始化table
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //索引處無值
            if (casTabAt(tab, i, null,
                         new Node(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)// MOVED=-1;
            //檢測到正在擴容,則幫助其擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //上鎖(hash值相同的鏈表的頭節點)
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        //遍歷鏈表節點
                        binCount = 1;
                        for (Node e = f;; ++binCount) {
                            K ek;
                            // hash和key相同,則修改value
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                //僅putIfAbsent()方法中onlyIfAbsent爲true
                                if (!onlyIfAbsent)
                                //putIfAbsent()包含key則返回get,不然put並返回
                                    e.val = value;
                                break;
                            }
                            Node pred = e;
                            //已遍歷到鏈表尾部,直接插入
                            if ((e = e.next) == null) {
                                pred.next = new Node(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {// 樹節點
                        Node p;
                        binCount = 2;
                        if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                //判斷是否要將鏈表轉換爲紅黑樹,臨界值和HashMap同樣也是8
                if (binCount >= TREEIFY_THRESHOLD)
                //若length<64,直接tryPresize,兩倍table.length;不轉樹
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}複製代碼

1. hash算法

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
複製代碼
2. table中定位索引位置,n是table的大小

int index = (n - 1) & hash複製代碼
3. 獲取table中對應索引的元素f。
採用Unsafe.getObjectVolatile來獲取。在java內存模型中,咱們已經知道每一個線程都有一個工做內存,裏面存儲着table的副本,雖然table是volatile修飾的,但不能保證線程每次都拿到table中的最新元素,Unsafe.getObjectVolatile能夠直接獲取指定內存的數據,保證了每次拿到數據都是最新的。

4. 若是f爲null,說明table中這個位置第一次插入元素,利用Unsafe.compareAndSwapObject方法插入Node節點。

  • 若是CAS成功,說明Node節點已經插入,break跳出,隨後addCount(1L, binCount)方法會檢查當前容量是否須要進行擴容。
  • 若是CAS失敗,說明有其它線程提早插入了節點,自旋從新嘗試在這個位置插入節點。
5. 若是f的hash值爲-1,說明當前f是ForwardingNode節點,意味有其它線程正在擴容,則一塊兒進行擴容操做。

6. 其他狀況把新的Node節點按鏈表或紅黑樹的方式插入到合適的位置,這個過程採用同步內置鎖實現併發,代碼如上。

在節點f上進行同步,節點插入以前,再次利用tabAt(tab, i) == f判斷,防止被其它線程修改。

  1. 若是f.hash >= 0,說明f是鏈表結構的頭結點,遍歷鏈表,若是找到對應的node節點,則修改value,不然在鏈表尾部加入節點。
  2. 若是f是TreeBin類型節點,說明f是紅黑樹根節點,則在樹結構上遍歷元素,更新或增長節點。
  3. 若是鏈表中節點數binCount >= TREEIFY_THRESHOLD(默認是8),則把鏈表轉化爲紅黑樹結構。

鏈表轉紅黑樹: treeifyBin()

treeifyBin 不必定就會進行紅黑樹轉換,也多是僅僅作數組擴容。咱們仍是看源碼吧。

private final void treeifyBin(Node[] tab, int index) {
    Node b; int n, sc;
    if (tab != null) {
        // MIN_TREEIFY_CAPACITY 爲 64
        // 因此,若是數組長度小於 64 的時候,其實也就是 32 或者 16 或者更小的時候,會進行數組擴容
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 後面咱們再詳細分析這個方法
            tryPresize(n << 1);
        // b 是頭結點
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            // 加鎖
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    // 下面就是遍歷鏈表,創建一顆紅黑樹
                    TreeNode hd = null, tl = null;
                    for (Node e = b; e != null; e = e.next) {
                        TreeNode p =
                            new TreeNode(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(hd));
                }
            }
        }
    }
}複製代碼

擴容:tryPresize()

若是說 Java8 ConcurrentHashMap 的源碼不簡單,那麼說的就是擴容操做和遷移操做。

這裏的擴容也是作翻倍擴容,擴容後數組容量爲原來的 2 倍。

// 首先要說明的是,方法參數 size 傳進來的時候就已經翻了倍了
private final void tryPresize(int size) {
    // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // 這個 if 分支和以前說的初始化數組的代碼基本上是同樣的
        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); // 0.75 * n
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {
            int rs = resizeStamp(n);
            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;
                // 2. 用 CAS 將 sizeCtl 加 1,而後執行 transfer 方法
                // 此時 nextTab 不爲 null
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 1. 將 sizeCtl 設置爲 (rs << RESIZE_STAMP_SHIFT) + 2)
            // 調用 transfer 方法,此時 nextTab 參數爲 null
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}複製代碼
這個方法的核心在於 sizeCtl 值的操做,首先將其設置爲一個負數,而後執行 transfer(tab, null),再下一個循環將 sizeCtl 加 1,並執行 transfer(tab, nt),以後多是繼續 sizeCtl 加 1,並執行 transfer(tab, nt)。

至於transfer()方法的源碼這裏我就不分析了,它的大概功能就是將原來的 tab 數組的元素遷移到新的 nextTab 數組中。

get()方法

get方法不用加鎖。利用CAS操做,能夠達到無鎖的訪問。

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) {//tabAt(i),獲取索引i處Node
        // 判斷頭結點是否就是咱們須要的節點
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 若是頭結點的 hash<0,說明正在擴容,或者該位置是紅黑樹
        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;
}複製代碼

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;
}複製代碼
get() 執行過程:
1. 計算 hash 值
2. 根據 hash 值找到數組對應位置: (n – 1) & h
3. 根據該位置處結點性質進行相應查找
  • 若是該位置爲 null,那麼直接返回 null 就能夠了
  • 若是該位置處的節點恰好就是咱們須要的,返回該節點的值便可
  • 若是該位置節點的 hash 值小於 0,說明正在擴容,或者是紅黑樹
  • 若是以上 3 條都不知足,那就是鏈表,進行遍歷比對便可

小結

到這裏我就基本把ConcurrentHashMap在 JDK 1.7和1.8中的實現大概捋了一遍,並詳細分析了幾個重要的方法實現:初始化、put、get。在 JDK1.8中ConcurrentHashMap發生了較大的變化,經過使用CAS+synchronized的實現取代了原先 1.7 中的Segment分段鎖機制,從而支持更高的併發量。

這只是我對ConcurrentHashMap的第二次學習,若想更好地理解掌握ConcurrentHashMap的實現之精妙,我的以爲還需之後再多看幾回,相信每次都會有新的收穫。

相關文章
相關標籤/搜索