【Java多執行緒】執行緒安全的集合

Sampson_S發表於2020-10-18

執行緒安全的集合

Vector

Vector集合是對ArrayList集合執行緒安全的實現,它們兩者在方法的實現上沒有什麼太大的區別,最大的區別就是,Vector在方法前面加上了synchronized關鍵字,用於保證執行緒安全。

Vector存在的問題:

  • 1、它的add()和get()方法都能夠獲取當前Vector物件的物件鎖,但是有可能會發生讀讀互斥。
  • 2、當threadA在1下標處新增一個元素,threadB在2下標處修改一個元素時,同樣有可能會發生互斥現象。
Vector v = new Vector(); 
thread1: v.add(100, 1); 
thread2: v.set(50, 2);

因此,我們可以看出Vector所存在的鎖的粒度是非常大的,這也就會導致在多執行緒情況下,程式執行的效率有可能會十分低下。

HashTable

HashTable集合是對HashMap集合執行緒安全的實現,它們兩者在方法的實現上沒有什麼太大的區別,最大的區別就是,HashTable在方法前面加上了synchronized關鍵字,用於保證執行緒安全。

HashTable存在的問題:

  • 由於HashTable和Vector在本質上都是在方法前面加上synchronized關鍵字,因此,它們兩個存在的問題也是同樣相同的,均有可能發生互斥現象。
  • 由此可知,HashTable所存在的鎖的粒度也是非常大的,也同樣會導致在多執行緒情況下,程式執行的效率有可能會十分低下。

為了解決Vector集合和HashTable集合效率低下的問題,我們在選取執行緒安全的集合時一般會選擇CopyOnWriteArrayList集合和ConcurrentHashMap集合,它的鎖的粒度相較於Vector和HashTable更小,因此能夠高效率的解決Vector和HashTable所存在的問題。

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裡結構

在這裡插入圖片描述

CurrentHashMap與HashMap的底層結構一致,都是基於陣列+連結串列+紅黑樹進行實現。

那麼它是如何保證執行緒安全的呢?

  • 答案:其中拋棄了原有JDK1.7的Segment分段鎖,而採用了 CAS + synchronized 來保證併發安全性。

現在我們來解決另一個問題,為什麼HashMap不是執行緒安全的。

  • 因為HashMap在多執行緒環境中很有可能產生死迴圈(注意不是死鎖)。

在連續的put時,會發生擴容,當擴容時就會產生讀取節點和移動節點,再次期間,並沒有對訪問進行一個控制,所以每一次在擴容時遍歷的節點,可能完全不相同,那麼這樣很有可能產生一個 A -> B -> A的情況,這樣就導致了死迴圈。

ConcurrentHashMap的重要變數以及方法

1、table

/**
 * The array of bins. Lazily initialized upon first insertion.
 * Size is always a power of two. Accessed directly by iterators.
 */
transient volatile Node<K,V>[] table;

裝載 Node 的陣列,作為 ConcurrentHashMap的資料容器,採用懶載入的方式,直到第一次插入資料的時候才會進行初始化操作,陣列的大小總是為 2 的冪次方。

2、nextTable

/**
 * The next table to use; non-null only while resizing.
 */
private transient volatile Node<K,V>[] nextTable;

擴容時新生成的陣列,大小為原陣列的2倍。平時為null,只有在擴容的時候才為非null。

3、sizeCtl

/**
 * Table initialization and resizing control.  When negative, the
 * table is being initialized or resized: -1 for initialization,
 * else -(1 + the number of active resizing threads).  Otherwise,
 * when table is null, holds the initial table size to use upon
 * creation, or 0 for default. After initialization, holds the
 * next element count value upon which to resize the table.
 */
private transient volatile int sizeCtl;

該屬性用來控制 table 陣列的大小,根據是否初始化和是否正在擴容有幾種情況:

  • 當值為負數時:如果為 -1 表示正在初始化 ,如果為 -N 則表示當前正有 N-1 個執行緒進行擴容操作。
  • 當值為正數時:如果當前陣列為 null 的話表示 table 在初始化過程中,sizeCtl 表示為需要新建陣列的長度。
  • 若已經初始化了,表示當前資料容器(table 陣列)可用容量也可以理解成臨界值(插入節點數超過了該臨界值就需要擴容),具體指為陣列的長度n 乘以 載入因子loadFactor
  • 預設值為0,當table被初始化後,sizeCtl的值為下一次要擴容時元素個數。
sun.misc.Unsafe U

在 ConcurrentHashMapde 的實現中可以看到大量的U.compareAndSwapXXXX 的方法去修改 ConcurrentHashMap 的一些屬性。這些方法實際上是利用了 CAS 演算法保證了執行緒安全性,這是一種樂觀策略,假設每一次操作都不會產生衝突,當且僅當衝突發生的時候再去嘗試。而 CAS 操作依賴於現代處理器指令集,通過底層CMPXCHG指令實現。

CAS(V,O,N)核心思想為:若當前變數實際值 V 與期望的舊值 O 相同,則表明該變數沒被其他執行緒進行修改,因此可以安全的將新值 N 賦值給變數;若當前變數實際值 V 與期望的舊值 O 不相同,則表明該變數已經被其他執行緒做了處理,此時將新值 N 賦給變數操作就是不安全的,在進行重試。而在大量的同步元件和併發容器的實現中使用 CAS 是通過sun.misc.Unsafe類實現的,該類提供了一些可以直接操控記憶體和執行緒的底層操作,可以理解為 java 中的“指標”。該成員變數的獲取是在靜態程式碼塊中:

static {
    try {
        U = sun.misc.Unsafe.getUnsafe();
        .......
    } catch (Exception e) {
        throw new Error(e);
    }
}
  • 也就是說通過Unsafe獲取的值,都是從主記憶體去獲取的。

4、Node

Node 類實現了 Map.Entry 介面,主要存放 key-value 對,並且具有 next 域。

/**
 * Key-value entry.  This class is never exported out as a
 * user-mutable Map.Entry (i.e., one supporting setValue; see
 * MapEntry below), but can be used for read-only traversals used
 * in bulk tasks.  Subclasses of Node with a negative hash field
 * are special, and contain null keys and values (but are never
 * exported).  Otherwise, keys and vals are never null.
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

    ......
}

5、TreeNode

樹節點,用於儲存紅黑樹節點,繼承於承載資料的 Node 類。而紅黑樹的操作是針對 TreeBin 類的,從該類的註釋也可以看出,也就是TreeBin 會將 TreeNode 進行再一次封裝。

/**
 * Nodes for use in TreeBins
 */
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;

    ......
}

6、TreeBin

這個類並不負責包裝使用者的 key、value 資訊,而是包裝的很多 TreeNode 節點。實際的 ConcurrentHashMap“陣列”中,存放的是 TreeBin 物件,而不是 TreeNode 物件。

/**
 * TreeNodes used at the heads of bins. TreeBins do not hold user
 * keys or values, but instead point to list of TreeNodes and
 * their root. They also maintain a parasitic read-write lock
 * forcing writers (who hold bin lock) to wait for readers (who do
 * not) to complete before tree restructuring operations.
 */
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

    ......
}

7、ForwardingNode

在擴容時才會出現的特殊節點,其 key,value,hash 全部為 null。並擁有 nextTable 指標引用新的 table 陣列。 可以把此節點看成標記節點,如果,table的某一個節點被標記為ForwardingNode,那麼此節點正在被一個執行緒執行擴容操作。

CAS操作:

//該方法用來獲取 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);
}

//利用 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);
}

//該方法用來設定 table 陣列中索引為 i 的元素,非CAS操作
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

ConcurrentHashMap的常用方法剖析

新增方法 put()

首先我們先看一張put方法的圖解。

相信看完這篇圖解,再讀原始碼就會由一個大致的瞭解了。

在這裡插入圖片描述

原始碼,以及解釋:

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) {
    if (key == null || value == null) throw new NullPointerException();
    //1. 計算key的hash值
    //spread(就是擾動函式),讓hashcode右移32位進行異或操作,來減少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;
}

初始化方法 initTable()

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            // 1.sizeCtl < 0表示其他執行緒也正在初始化,
            //保證只有一個執行緒正在進行初始化操作,所以讓出時間片
            Thread.yield(); // lost initialization race; just spin
            //沒有其他執行緒進行操作,那麼就直接將sizeCtl置為-1。
        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;
}

ConcurrentHashMap的擴容

通過判斷該節點的hash值是不是等於-1(MOVED),程式碼為(fh = f.hash) == MOVED,說明 Map 正在擴容。那麼就幫助 Map 進行擴容。以加快速度。

helpTransfer(Node<K,V>[] tab, Node<K,V> f)就是協助擴容的方法。這裡我們就能看出ConcurrentHashMap設計的精妙之處了,執行緒不僅可以進行增刪改查,甚至可以去協助擴容,來減少擴容時移動資料的大量操作對阻塞時間的影響。讓多個執行緒一起完成擴容,使得擴容速度非常的快,不僅僅減少了擴容需要的時間,還合理的利用了執行緒資源。這種想法屬實太強了。

首先我們來看一下作為擴容的入口點,也就是什麼時候擴容呢?

  • 就是當節點的個數等於 SizeCtl 的時候擴容,擴容依舊是2倍擴容。那麼統計節點個數的方法就是擴容方法的入口點。也就是addCount()。

addCount()方法

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //通過CAS更新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();
        }
    }
}

實際上addCount的原理,很簡單,統計並更新所有節點個數,更新時使用的是CAS操作。然後進行檢查,檢視當前是否需要擴容,如果需要擴容,進入transfer()方法中。

transfer()方法

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
 //1. 新建Node陣列,容量為之前的兩倍
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
 //2. 新建forwardingNode引用,在之後會用到
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 3. 確定遍歷中的索引i
  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;
            }
        }
  //4.將原陣列中的元素複製到新陣列中去
  //4.5 for迴圈退出,擴容結束脩改sizeCtl屬性
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            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
            }
        }
  //4.1 當前陣列中第i個元素為null,用CAS設定成特殊節點forwardingNode(可以理解成佔位符)
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
  //4.2 如果遍歷到ForwardingNode節點  說明這個點已經被處理過了 直接跳過  這裡是控制併發擴容的核心
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
      //4.3 處理當前節點為連結串列的頭結點的情況,構造兩個連結串列,一個是原連結串列  另一個是原連結串列的反序排列
                        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節點  表示已經處理過該節點
                       setTabAt(tab, i, fwd);
                       //設定advance為true 返回到上面的while迴圈中 就可以執行i--操作
                       advance = true;
                    }
     //4.4 處理當前節點是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;
                            }
                        }
                        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;
                    }
                }
            }
        }
    }
}

程式碼邏輯請看註釋,整個擴容操作分為兩個部分:

  • 第一部分:構建一個 nextTable,它的容量是原來的兩倍,這個操作是單執行緒完成的。新建 table 陣列的程式碼為:Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1],在原容量大小的基礎上右移一位。
  • 第二個部分:就是將原來 table 中的元素複製到 nextTable 中,主要是遍歷複製的過程。

根據運算得到當前遍歷的陣列的位置 i,然後利用 tabAt 方法獲得 i 位置的元素再進行判斷:

  • 1、如果這個位置為空,就在原 table 中的 i 位置放入 forwardNode 節點,這個也是觸發併發擴容的關鍵點。
  • 2、如果這個位置是 Node 節點(fh>=0),如果它是一個連結串列的頭節點,就構造一個反序連結串列,把他們分別放在 nextTable 的 i 和 i+n 的位置上。
  • 3、如果這個位置是 TreeBin 節點(fh<0),也做一個反序處理,並且判斷是否需要 untreefi,把處理的結果分別放在 nextTable 的 i 和 i+n 的位置上。
  • 4、遍歷過所有的節點以後就完成了複製工作,這時讓 nextTable 作為新的 table,並且更新 sizeCtl 為新容量的 0.75 倍 ,完成擴容。

設定為新容量的 0.75 倍程式碼為 sizeCtl = (n << 1) - (n >>> 1),仔細體會下是不是很巧妙,n<<1 相當於 n 左移一位表示 n 的兩倍即 2n,n>>>1,n 右移相當於 n 除以 2 即 0.5n,然後兩者相減為 2n-0.5n=1.5n,是不是剛好等於新容量的 0.75 倍即 2n*0.75=1.5n。

HashMap、Hashtable、ConccurentHashMap三者的區別

  • HashMap:非執行緒安全,允許NULL值與NULL鍵。預設大小為16,擴容為2倍擴容。
  • HashTable:執行緒安全,不允許NULL值與NULL鍵,預設大小為11,擴容為2倍+1擴容。HashTable的執行緒安全實現依靠Synchronized。
  • ConcurrentHashMap:執行緒安全,不允許NULL值與NULL鍵,預設大小為16,擴容為2倍擴容。ConcurrentHashMap的執行緒安全實現依靠於Synchronized + CAS 。

HashMap不應用於併發場景,會產生死迴圈,HashTable於ConcurrentHashMap運用於併發場景,但是兩者有效能差距。當資料量足夠大時,我們會發現ConcurrentHashMap的效率實際上比HashTable要低下一些,但是關於讀操作,ConcurrentHashMap比HashTable快不止一個量級。

所以適用於什麼場景關鍵還是需要進行壓力測試,才可以斷言需要用什麼樣的容器。

  • (Collections.synchronizedMap(new HashMap()); 可以將普通的hashMap變為執行緒安全的HashMap。)

ConcurrentHashMap在JDK7和JDK8的區別

ConcurrentHashMap在JDK7版本中實現的是Segment分段鎖,一個Segment鎖上一個或者幾個table節點。當要對指定的節點上的資料進行操作時,先獲取對應的Segement的鎖才可以,而這種鎖的粒度相對較大,並且採用 ReetrantLock 的方式去獲取與釋放鎖。

JDK8版本中實現拋棄了原來的Segment分端鎖,轉而用鎖table的節點,也就是鎖連結串列頭或者 樹的根節點。這種轉變直接將鎖的粒度變小,使得執行緒的衝突變少,並且支援多執行緒協助擴容,使用3個CAS操作來確保 node 的一些操作的原子性,這種方式代替了鎖。

相關文章