ConcurrentHashMap集合源碼學習筆記

1、概念

ConcurrentHashMap繼承了AbstractMap,實現了ConcurrentMap,Serializable接口,ConcurrentMap又實現了Map接口。ConcurrentHashMap是基於散列表實現的,存儲的是Key/Value對,底層使用數組+鏈表+紅黑樹+CAS算法實現的,數組是存儲元素並且查找快,鏈表是爲了解決哈希衝突而存在的,紅黑樹是爲了解決鏈表中查詢速度慢而使用的。CAS可以彌補HashMap線程不安全的缺點,使ConcurrentHashMap實現線程安全。

2、空間結構

對象序列化的UID,類序列化時會傳入一個serialVersionUID。在反序列化時,JVM會把傳來的字節流中的serialVersionUID於本地相應實體類的serialVersionUID進行比較。如果相同說明是一致的,可以進行反序列化,否則會出現反序列化版本一致的異常,即是InvalidCastException。

    private static final long serialVersionUID = 7249069246763182397L;

默認的初始容量大小爲16。

    private static final int DEFAULT_CAPACITY = 16;

默認最大容量爲1左移30位,aka 2的30次方。最好是通過構造函數指定,以免多次擴容影響效率。

    private static final int MAXIMUM_CAPACITY = 1 << 30;

默認的裝載因子0.75,即當容量到達默認容量*裝載因子時就會擴容。

    private static final float LOAD_FACTOR = 0.75f;

默認的鏈表長度達到8以後,鏈表就會轉化爲紅黑樹(實際上後面還有一個判斷條件)。

	static final int TREEIFY_THRESHOLD = 8;

默認當紅黑樹元素個數將爲6個時,轉換回鏈表。

	static final int UNTREEIFY_THRESHOLD = 6;

默認的當數組長度小於這個值時,會先進行擴容稀釋一個鏈表存儲位置,當數組長度大於64且鏈表長度大於8時,就會轉換爲紅黑樹。

	static final int MIN_TREEIFY_CAPACITY = 64;

HashMap中存儲這些Node<K,V>所使用的數組。使用transient修飾跟ArrayList類似,節省序列和時不必要的空間,同時避免在不同JVM中hashcode方法不同導致桶位置不一樣。

transient Node<K,V>[] table;

擴容時使用,平時爲 null,只有在擴容的時候才爲非 null

  private transient volatile Node<K,V>[] nextTable;

該屬性用來控制 table 數組的大小,根據是否初始化和是否正在擴容有幾種情況:
當值爲負數時:如果爲-1 表示正在初始化,如果爲-N 則表示當前正有 N-1 個線程進行擴容操作;
當值爲正數時:如果當前數組爲 null 的話表示 table 在初始化過程中,sizeCtl 表示爲需要新建數組的長度;
若已經初始化了,表示當前數據容器(table 數組)可用容量也可以理解成臨界值(插入節點數超過了該臨界值就需要擴容),具體指爲數組的長度 n 乘以 加載因子 loadFactor;
當值爲 0 時,即數組長度爲默認初始值。

private transient volatile int sizeCtl;

Node 類實現了 Map.Entry 接口,存放包括hash值、key、value和鏈表的指針,重寫了equals方法,與HashMap不同的是,屬性加了volatile修飾,保證內存可見性。

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 樹節點,繼承於承載數據的 Node 類。而紅黑樹的操作是針對 TreeBin 類的,TreeBin 會將 TreeNode 進行再一次封裝。

static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    
    boolean red;
    ......
}

TreeBin 這個類並不負責包裝用戶的 key、value 信息,而是包裝的很多 TreeNode 節點。實際的 ConcurrentHashMap“數組”中,存放的是 TreeBin 對象,而不是 TreeNode 對象。

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; 
	        static final int WAITER = 2; 
	        static final int READER = 4; 
			......
	}

ConcurrentHashMap的大概結構與hashMap類似。
在這裏插入圖片描述

3、常用方法

構造函數
一共提供瞭如下幾個構造器方法:

// 1. 構造一個空的map,即table數組還未初始化,初始化放在第一次插入數據時,默認大小爲16
ConcurrentHashMap()
// 2. 給定map的大小
ConcurrentHashMap(int initialCapacity)
// 3. 給定一個map
ConcurrentHashMap(Map<? extends K, ? extends V> m)
// 4. 給定map的大小以及加載因子
ConcurrentHashMap(int initialCapacity, float loadFactor)
// 5. 給定map大小,加載因子以及併發度(預計同時操作數據的線程)
ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)

解析第二種構造函數:如果容量小於0直接拋出異常,大於了指定最大值則取指定最大值,否則對容量進行tableSizeFor處理,賦給sizeCtl。

public ConcurrentHashMap(int initialCapacity) {
	//1. 小於0直接拋異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
	//2. 判斷是否超過了允許的最大值,超過了話則取最大值,否則再對該值進一步處理
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
	//3. 賦值給sizeCtl
    this.sizeCtl = cap;
}
  1. tableSizeFor具體做法就是把當前容量的二進制最高位右邊的位都填上1。實際上返回了一個比給定容量大且接近2的冪次方的一個整數。借用一張圖來演示過程。
    在這裏插入圖片描述
	//返回一個比給定容量大且接近2的冪次方的一個整數。
    static final int tableSizeFor(int cap) {
    	//減1防止cap剛好是2的冪次方數。
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

tabAt
該方法用來獲取 table 數組中索引爲 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);
    }

casTabAt
利用 CAS 操作設置 table 數組中索引爲 i 的元素

    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);
    }

setTabAt
該方法用來設置 table 數組中索引爲 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
該方法用來初始化map,該方法可能存在多線程同時訪問。爲了保證能夠正確初始化,在第 1 步中會先通過 if 進行判斷,若當前已經有一個線程正在初始化即 sizeCtl 值變爲-1,這個時候其他線程在 If 判斷爲 true 從而調用 Thread.yield()讓出 CPU 時間片。正在進行初始化的線程會調用 U.compareAndSwapInt 方法將 sizeCtl 改爲-1 即正在初始化的狀態。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
			// 保證只有一個線程正在進行初始化操作
            Thread.yield(); 
        // CAS 一下,將 sizeCtl 設置爲 -1,代表搶到了鎖
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
					// 得出數組的大小,DEFAULT_CAPACITY 默認初始容量是 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
					// 初始化數組,長度爲 16 或初始化時提供的長度
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 將這個數組賦值給 table,table 是 volatile 的
                    table = tab = nt;
					// 4. 計算數組中可用的大小:實際大小n*0.75(加載因子)
                    sc = n - (n >>> 2);
                }
            } finally {
            	// 設置 sizeCtl 爲 sc,即數組中可用大小
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

putVal
整體思路和hashMap的put操作相似,也要分爲數組下標無節點、鏈表和紅黑樹,爲了解決線程安全的問題,ConcurrentHashMap 使用了 synchronzied 和 CAS 的方式。
整體流程:

  1. 首先對於每一個放入的值,首先利用 spread 方法對 key 的 hashcode 進行一次 hash 計算,由此來確定這個值在 table 中的位置;
  2. 如果當前 table 數組還未初始化,先將 table 數組進行初始化操作;
  3. 如果這個位置是 null 的,那麼使用 CAS 操作直接放入;
  4. 如果這個位置存在結點,說明發生了 hash 碰撞,首先判斷這個節點的類型。如果該節點 fh==MOVED(代表 forwardingNode,數組正在進行擴容)的話,說明正在進行擴容;
  5. 如果是鏈表節點(fh>0),則得到的結點就是 hash 值相同的節點組成的鏈表的頭節點。需要依次向後遍歷確定這個新加入的值所在位置。如果遇到 key 相同的節點,則只需要覆蓋該結點的 value 值即可。否則依次向後遍歷,直到鏈表尾插入這個結點;
  6. 如果這個節點的類型是 TreeBin 的話,直接調用紅黑樹的插入方法進行插入新的節點;
  7. 插入完節點之後再次檢查鏈表長度,如果長度大於 8,就把這個鏈表轉換成紅黑樹;
  8. 對當前容量大小進行檢查,如果超過了臨界值(實際大小*加載因子)就需要擴容。
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
	// 得到 hash 值
    int hash = spread(key.hashCode());
    // 用於記錄相應鏈表的長度
    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();
		// 找該 hash 值對應的數組下標,得到第一個節點 f
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        
            // 如果數組該位置爲空,用CAS 操作將這個新值放入其中即可
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   
        }
		//當前正在擴容
        else if ((fh = f.hash) == MOVED)
       	    // 幫助數據遷移,這個等到看完數據遷移部分的介紹後,再理解這個就很簡單了
            tab = helpTransfer(tab, f);
        //f 是該位置的頭結點,而且不爲空
        else {
            V oldVal = null;
            // 獲取數組該位置的頭結點的監視器鎖
            synchronized (f) {
                if (tabAt(tab, i) == f) {
					//因爲fh>0,所以當前爲鏈表,在鏈表中插入新的鍵值對
                    if (fh >= 0) {
                  	    // 用於累加,記錄鏈表的長度
                        binCount = 1;
                        // 遍歷鏈表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果發現了"相等"的 key,判斷是否要進行值覆蓋,然後也就可以 break 了
                            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;
                        }
                    }
                }
            }
			// 7.插入完鍵值對後再根據實際大小看是否需要轉換成紅黑樹
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
	//對當前容量大小進行檢查,如果超過了臨界值(實際大小*加載因子)就需要擴容
    addCount(1L, binCount);
    return null;
}

get
整體流程:

  1. 計算 hash 值
  2. 根據 hash 值找到數組對應位置: (n - 1) & h
  3. 根據該位置處結點性質進行相應查找
    • 如果該位置爲 null,那麼直接返回 null 就可以了
    • 如果該位置處的節點剛好就是我們需要的,返回該節點的值即可
    • 如果該位置節點的 hash 值小於 0,說明正在擴容,或者是紅黑樹
    • 如果以上 3 條都不滿足,那就是鏈表,進行遍歷比對即可
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
	// 1. 重hash
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 2. table[i]桶節點的key與查找的key相同,則直接返回
		if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
		// 3. 當前節點hash小於0說明爲樹節點,在紅黑樹中查找即可
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
		//4. 從鏈表中查找,查找到則返回該節點的value,否則就返回null即可
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

擴容操作

兩種觸發擴容條件:

  1. 如果新增節點之後,所在鏈表的元素個數達到了閾值 8,則會調用treeifyBin方法把鏈表轉換成紅黑樹,不過在結構轉換之前,會對數組長度進行判斷,如果數組長度n小於閾值MIN_TREEIFY_CAPACITY,默認是64,則會調用tryPresize方法把數組長度擴大到原來的兩倍,並觸發transfer方法,重新調整節點的位置。
  2. 新增節點之後,會調用addCount方法記錄元素個數,並檢查是否需要進行擴容,當數組元素個數達到閾值時,會觸發transfer方法,重新調整節點的位置。

整個擴容操作分爲兩個部分:

  1. 構建一個 nextTable,它的容量是原來的兩倍,這個操作是單線程完成的。新建 table 數組的代碼爲:Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1],在原容量大小的基礎上右移一位。

  2. 將原來 table 中的元素複製到 nextTable 中,主要是遍歷複製的過程。
    根據運算得到當前遍歷的數組的位置 i,然後利用 tabAt 方法獲得 i 位置的元素再進行判斷:

    1. 如果這個位置爲空,就在原 table 中的 i 位置放入 forwardNode 節點。
    2. 如果這個位置是 Node 節點(fh>=0),使用fn&n可以快速把鏈表中的元素區分成兩類,A類是hash值的第X位爲0,B類是hash值的第X位爲1,並通過lastRun記錄最後需要處理的節點。把他們分別放在 nextTable 的 i 和 i+n 的位置上
    3. 如果這個位置是 TreeBin 節點(fh<0),也做一個反序處理,並且判斷是否需要 untreefi,把處理的結果分別放在 nextTable 的 i 和 i+n 的位置上
    4. 遍歷過所有的節點以後就完成了複製工作,這時讓 nextTable 作爲新的 table,並且更新 sizeCtl 爲新容量的 0.75 倍 ,完成擴容。

節點遷移示意圖:
遷移之前的節點:
在這裏插入圖片描述
使用fn&n可以快速把鏈表中的元素區分成兩類,A類是hash值的第X位爲0,B類是hash值的第X位爲1,並通過lastRun記錄最後需要處理的節點。
ln鏈:
在這裏插入圖片描述
hn鏈:
在這裏插入圖片描述
通過CAS把ln鏈表設置到新數組的i位置,hn鏈表設置到i+n的位置;

源碼如下:

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //單核時stride等於n,多核時爲(n >>> 3) / NCPU,stride 可以理解爲”步長“,有 n 個位置是需要進行遷移的,將這 n 個任務分爲多個任務包,每個任務包有 stride 個任務
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; 
	//新建Node數組,容量爲之前的兩倍
    if (nextTab == null) {          
        try {
       		// 容量翻倍
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {     
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        //用於擴容遷移使用
        nextTable = nextTab;
        //用於控制遷移的位置
        transferIndex = n;
    }
    int nextn = nextTab.length;
	//新建forwardingNode引用,在之後會用到
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // advance 指的是做完了一個位置的遷移工作,可以準備做下一個位置的了
    boolean advance = true;
    boolean finishing = false;
    // i 是位置索引,bound 是邊界
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 確定遍歷中的索引i,i 指向了 transferIndex,bound 指向了 transferIndex-stride
		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;
            }
        }
		//將原數組中的元素複製到新數組中去
		//for循環退出,擴容結束脩改sizeCtl屬性
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                // 所有的遷移操作已經完成
                nextTable = null;
                // 將新的 nextTab 賦值給 table 屬性,完成遷移
                table = nextTab;
                // 重新計算 sizeCtl: n 是原數組長度,所以 sizeCtl 得出的值將是新數組長度的 0.75 倍
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            //使用 CAS 操作對 sizeCtl 進行減 1,代表做完了屬於自己的任務
            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
            }
        }
		//當前數組中第i個元素爲null,用CAS設置成特殊節點forwardingNode(可以理解成佔位符)
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
		//如果遍歷到ForwardingNode節點  說明這個點已經被處理過了 直接跳過  這裏是控制併發擴容的核心
        else if ((fh = f.hash) == MOVED)
            advance = true; 
        else {
        	// 對數組該位置處的結點加鎖,開始處理數組該位置處的遷移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 頭結點的 hash 大於 0,說明是鏈表的 Node 節點
                    if (fh >= 0) {
						// 處理當前節點爲鏈表的頭結點的情況,構造兩個鏈表,找到原鏈表中的 lastRun,然後 lastRun 及其之後的節點是一起進行遷移的, lastRun 之前的節點需要進行克隆,然後分到兩個鏈表中
                        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位置上插入forwardNode節點  表示已經處理過該節點,其他線程一旦看到該位置的 hash 值爲 MOVED,就不會進行遷移了
                       setTabAt(tab, i, fwd);
                       //設置advance爲true 代表該位置已經遷移完畢,返回到上面的while循環中 就可以執行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;
                            }
                        }
                        // 如果一分爲二後,節點數少於 8,那麼將紅黑樹轉換回鏈表
                        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;
                        // 將 ln 放置在新數組的位置 i
                        setTabAt(nextTab, i, ln);
                        // 將 hn 放置在新數組的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,其他線程一旦看到該位置的 hash 值爲 MOVED,就不會進行遷移了
                        setTabAt(tab, i, fwd);
                        // advance 設置爲 true,代表該位置已經遷移完畢
                        advance = true;
                    }
                }
            }
        }
    }
}

4、總結

HashTable 和同步包裝器包裝的 HashMap,使用一個全局的鎖來同步不同線程間的併發訪問,導致對容器的訪問變成串行化的了。ConcurrentHashMap 是一個併發散列映射表的實現。1.7與1.8的ConcurrentHashMap對比:

  1. 數據結構:取消了Segment分段鎖的數據結構,取而代之的是數組+鏈表+紅黑樹的結構。
  2. 保證線程安全機制:JDK1.7採用segment的分段鎖機制實現線程安全,其中segment繼承自ReentrantLock。JDK1.8採用CAS+Synchronized保證線程安全。
  3. 鎖的粒度:原來是對需要進行數據操作的Segment加鎖,現調整爲對每個數組元素加鎖(Node)。
  4. 鏈表轉化爲紅黑樹:定位結點的hash算法簡化會帶來弊端,Hash衝突加劇,因此在鏈表節點數量大於8時,會將鏈表轉化爲紅黑樹進行存儲。
  5. 查詢時間複雜度:從原來的遍歷鏈表O(n),變成遍歷紅黑樹O(logN)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章