JAVA 多線程與高并發學習筆記(十五)——JUC容器

Java 中的容器類主要有 List、Set、Queue和Map,但他們的基礎實現比如 ArrayList、HashMap 是線程不安全的。Java內置鎖提供了一套線程安全的同步容器類雖然同步容器類解決了線程安全問題,不過性能不高。JUC提供了一套高并發容器類。

線程安全的同步容器類

Java 同步容器類通過 Synchronized(內置鎖)來實現同步的容器,比如Vector、HashTable等。另外,Java還可以通過包裝方法將一個普通的基礎容器包裝成一個線程安全的同步容器。

下面是通過 synchronizedSortedSet 靜態方法包裝出一個同步容器的具體實現:

public class CollectionDemo {

    public static void main(String[] args) throws InterruptedException {
        // 創建一個基礎的有序集合
        SortedSet<String> elementSet = new TreeSet<>();

        // 增加元素
        elementSet.add("element 1");
        elementSet.add("element 2");

        // 將 elementSet 包裝成一個同步容器
        SortedSet sorSet = Collections.synchronizedSortedSet(elementSet);
        // 輸出容器中的元素
        System.out.println("SortedSet is : " + sorSet);
        CountDownLatch latch = new CountDownLatch(5);

        for(int i = 0; i < 5; i++) {
            int finalI = i;
            ExecutorService executor = Executors.newCachedThreadPool();
            executor.submit(() -> {
                sorSet.add("element " + (3 + finalI));
                System.out.println("add element" + (3 + finalI));
                latch.countDown();
            });
        }
        latch.await();
        System.out.println("SortedSet is : " + sorSet);
    }
}

運行程序,輸出結果如下:

synchronizedSortedSet.png

另外,還包含 synchronizedList、synchronizedMap 等方法。

同步容器使用了關鍵字 synchronized,它如前面所介紹,在線程沒有發生爭用的場景下處于偏向鎖的狀態,其性能是非常高的。但是一旦發生了線程爭用,synchronized 會由偏向鎖膨脹成重量級鎖,在搶占和釋放時發生 CPU 內核態與用戶態切換,所以削弱了并發性,降低了吞吐量,而且會嚴重影響性能。

為了解決同步容器的性能問題,有了JUC高并發容器。

JUC高并發容器

JUC 高并發容器是基于非阻塞算法(或無鎖編程算法)實現的容器類,無鎖編程算法主要通過CAS + volatile 組合實現,通過CAS保障操作的原子性,通過 volatile 保障變量內存內的可見性,它主要有以下優點:

  1. 開銷比較小,不需要在內核態和用戶態之間切換進程。
  2. 讀寫不互斥,只有寫操作需要使用基于CAS機制的樂觀鎖,讀操作之間可以不用互斥。

List

JUC 包中的高并發 List 主要有 CopyOnWriteArrayList,對應的基礎容器為 ArrayList。在讀多寫少的場景下,其性能遠遠高于 ArrayList 的同步包裝器。

Set

JUC 包中的 Set 主要有 CopyOnWriteArraySet 和 CocurrentSkipListSet。

  • CopyOnWriteArraySet 對應的基礎容器為 HashSet,其內部組合了一個 CopyOnWriteArrayList 對象,它的核心操作是基于 CopyOnWriteArrayList 實現的。

  • CocurrentSkipListSet是線程安全的有序集合,對應的基礎容器為 TreeSet。它是通過 CocurrentSkipListMap 實現的。

Map

JUC 包中 Map 主要有 ConcurrentHashMap 和 ConcurrentSkipListMap。

  • ConcurrentHashMap 對應的基礎容器為 HashMap。JDK6 中的 ConcurrentHashMap 采用了一種更加細粒度的“分段鎖”加鎖機制,JDK8采用CAS無鎖算法。

  • ConcurrentSkipListMap 對應的基礎容器為 TreeMap。其內部的 SkipList(跳表)結構是一種可以代替平衡樹的數據結構,默認是按照Key值升序的。

Queue

JUC包中的Queue的實現類包括三類,單向隊列、雙向隊列和阻塞隊列。

  • ConcurrentLinkedQueue 是基于列表實現的單向隊列,按照FIFO(先進先出)原則對元素進行排序,新元素從隊列尾部插入,而獲取隊列元素則需要性隊列頭部獲取。

  • ConcurrentLinkedDeque 是基于鏈表的雙向隊列,但是該隊列不允許 null 元素,作為雙向隊列,ConcurrentLinkedDeque 可以作為棧使用,并且高效的支持并發環境。

除了單向隊列和雙向隊列,JUC拓展了隊列,增加了可阻塞的插入和獲取等操作,提供了一組阻塞隊列,具體如下:

  • ArrayBlockingQueue:基于數組實現的可阻塞的FIFO隊列。
  • LinkedBlockingQueue:基于鏈表實現的可阻塞的FIFO隊列。
  • PriorityBlockingQueue:按優先級排序的隊列。
  • DelayQueue:按照元素的delay時間進行排除的隊列。
  • SynchronousQueue:無緩沖的等待隊列。

CopyOnWriteArrayList

在很多應用場景中,讀操作會遠遠大于寫操作。由于讀操作不修改數據,我們可以允許多個線程同時訪問。

寫時復制(Copy On Write,COW)思想是計算程序設計領域的一種優化策略,具體思想是,如果有多個訪問器(Accessor)訪問一個資源,他們會共同獲取相同的指針指向相同的資源,只要有一個修改器(Mutator)需要修改該資源,系統就會復制一個專用副本(Private Copy)給該修改器,而其它訪問器所見到的最初資源仍然保持不變,修改的過程對其他訪問器都是透明的(Transparently)。

CopyOnWriteArrayList 原理

CopyOnWrite容器即寫時復制的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,復制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行并發的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

CopyOnWriteArrayList 的原理如下圖所示:

copyonwritearraylist.jpeg

CopyOnWriteArrayList 是一個滿足 CopyOnWrite 思想并使用 Array 數組存儲數據的線程安全 List,CopyOnWriteArrayList 的核心成員如下:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** 對所有的修改器方法進行保護,訪問器方法并不需要保護*/
    final transient ReentrantLock lock = new ReentrantLock();

    /** 內部對象數組 */
    private transient volatile Object[] array;

    /**
     * 獲取內部對象數組
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * 設置內部對象數組
     */
    final void setArray(Object[] a) {
        array = a;
    }

...
}

CopyOnWriteArrayList 讀取操作

訪問器的讀取操作并沒有同步控制和鎖操作,理由是內部數組 array 不會發生修改,只會被另一個 array 替換,因此可以保證數據安全。

 private E get(Object[] a, int index) {
        return (E) a[index];
    }

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }

CopyOnWriteArrayList 寫入操作

CopyOnWriteArrayList 的寫入操作 add 方法在執行時加了獨占鎖以確保只能有一個線程進行寫入操作,避免多線程寫的時候會復制出多個副本。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加鎖
    try {
        Object[] elements = getArray();
        int len = elements.length;

        // 復制新數組
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock(); // 釋放鎖
    }
}

可以看到其中重新復制一份數組,再往新的數組添加元素,待添加完了,再將新的 array 引用指向新的數組。

CopyOnWriteArrayList 的迭代器實現

CopyOnWriteArrayList 有自己的迭代器,該迭代器不會檢查修改狀態,也無需加您查狀態,因為 array 數組是只讀的。

static final class COWIterator<E> implements ListIterator<E> {
    /** 對象數組的快照 */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }
    ...
}

迭代器的快照成員會在構造迭代器的時候使用 CopyOnWriteArrayList 的 array 成員去初始化。

public ListIterator<E> listIterator() {
    return new COWIterator<E>(getArray(), 0);
}

CopyOnWriteArrayList 和 ReentrantReadWriteLock 讀寫鎖的思想非常類似,即讀讀共享、寫寫互斥、讀寫互斥、寫讀互斥。但前者的讀取完全不用加鎖,寫入也不會阻塞讀取操作,提升了性能。

BlockingQueue

在多線程環境中,通過 BlockingQueue 可以很容易實現多線程之間的數據共享和通信。

BlockingQueue 特點

阻塞隊列和普通隊列最大不同是阻塞隊列提供了阻塞式的添加和刪除方法。

  1. 阻塞添加。當阻塞隊列元素已滿時,隊列會阻塞添加元素的線程,直到對了元素不滿時,才重新喚醒線程執行元素添加操作。

  2. 阻塞刪除。阻塞刪除是指在隊列元素為空時,刪除隊列元素的線程將被阻塞,直到隊列不為空時,才重新喚醒刪除線程,再執行刪除操作。

阻塞隊列的常用方法

public interface BlockingQueue<E> extends Queue<E> {
    // 將指定的元素添加到此隊列尾部
    // 在成功時返回true,如果隊列已滿,就拋出 IllegalStateException
    boolean add(E e);

    // 非阻塞添加,將指定的元素添加到此隊列的尾部
    // 如果該隊列已滿,就直接返回
    boolean offer(E e);

    // 限時阻塞式添加,將指定的元素添加到此隊列的尾部
    // 如果該隊列已滿,那么在到達指定的等待時間之前,添加線程會則色,等待可用的空間。該方法可中斷
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    // 阻塞式添加:將指定的元素添加到此隊列的尾部,如果隊列已滿,就一直等待
    void put(E e) throws InterruptedException;

    // 阻塞式刪除:獲取并移除此隊列的頭部,如果沒有元素就等待
    // 直到有元素,將喚醒等待線程執行該操作
    E take() throws InterruptedException;

    // 非阻塞式刪除:獲取并移除此隊列的頭部,如果沒有元素就直接返回null
    E poll() throws InterruptedException;

    // 限時阻塞式刪除:獲取并移除此隊列的頭部,在指定的等待時間前一直等待獲取元素,超過時間,方法結束
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    // 獲取但不移除此隊列的頭元素,沒有則拋出異常NoSuchElementException
    E element();

    // 獲取但不溢出此隊列的頭元素,如果此隊列為空,就返回null
    E peek();

    // 從此隊列中移除指定元素,返回刪除是否成功
    boolean remove(Object o);
}

方法總結:

拋出異常 特殊值 阻塞 限時阻塞
添加 add(e) offer(e) put(e) offer(e, time, unit)
刪除 remove() poll() take() poll(time, unit)
獲取 element() peek()

特征說明:

  • 拋出異常:如果操作無法立即執行,就拋出一個異常。
  • 特殊值,如果操作無法立即執行,就返回一個特殊值(一般是true、false)。
  • 阻塞,如果操作無法立即執行,就會發生阻塞,直到能夠執行。
  • 限時阻塞,如果操作無法立即執行,就會發生阻塞,直到能夠執行,但等待時間不會超過設置的上限值。

常見的 BlockingQueue

ArrayBlockingQueue

ArrayBlockingQueue 是一個常用的阻塞隊列,基于數組實現。它的內部還保存著兩個整形變量,標識隊列的頭部和尾部在數組中的位置。

ArrayBlockingQueue 的添加和刪除操作共用同一個鎖對象,這意味著添加和刪除操作無法并行運行。

由于 ArrayBlockingQueue 在添加或刪除元素時不會產生或銷毀任何額外的Node實例,所以相比 LinkedBlockingQueue 更加常用。

LinkedBlockedQueue

LinkedBlockedQueue 是一個基于鏈表的阻塞隊列,它對于添加和刪除元素分別采用了獨立的鎖來控制。

需要注意的是,在新建 LinkedBlockedQueue 對象是,如果沒有指定容量,則默認容量為 Intger.MAX_VALUE。

DelayQueue

DelayQueue 中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取該元素,它沒有大小限制。

DelayQueue 使用場景較少,常見的例子是使用 DelayQueue 來管理一個超時未響應的連接隊列。

PriorityBlockingQueue

基于優先級的阻塞隊列。

SynchronousQueue

無緩沖的等待隊列。

ArrayBlockingQueue 介紹

ArrayBlockingQueue 可以用作公平隊列和非公平隊列。

  1. 對于公平隊列,被阻塞的線程可以按照阻塞的先后順序訪問隊列,即先阻塞的線程先訪問隊列。

  2. 對于非公平隊列,當隊列可用時,阻塞的線程將進入爭奪訪問資源的競爭中,也就是說誰先搶到誰就執行,沒有固定的先后順序。

ArrayBlockingQueue 構造器

創建公平和非公平阻塞隊列的示例代碼如下:

// 默認非公平阻塞隊列
ArrayBlockingQueue queue = new ArrayBlockingQueue(10);

// 公平阻塞隊列
ArrayBlockingQueue queue = new ArrayBlockingQueue(10, true);

兩個構造器的源碼:

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    // 根據fair參數構造公平鎖/非公平鎖
    lock = new ReentrantLock(fair);
    // 有元素加入,隊列為非空
    notEmpty = lock.newCondition();
    // 有元素被取出,隊列為未滿
    notFull =  lock.newCondition();
}

ArrayBlockingQueue 內部的阻塞隊列是通過重入鎖 ReentrantLock 和 Condition 條件隊列實現的。

ArrayBlockingQueue 內部成員變量

ArrayBlockingQueue 成員如下:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {


    /** 存儲數據的數組 */
    final Object[] items;

    /** 獲取、刪除元素的索引,主要用于take、pool、peek、remove方法 */
    int takeIndex;

    /** 添加元素的索引,主要用于put、offer、add方法 */
    int putIndex;

    /** 隊列元素的個數 */
    int count;

    /** 控制并發訪問的顯式鎖 */
    final ReentrantLock lock;

    /** notEmpty條件對象,用于通知消費線程可執行刪除操作 */
    private final Condition notEmpty;

    /** notNull條件對象,用于通知生產線程可執行添加操作 */
    private final Condition notFull;

    /**
     * 迭代器
     */
    transient Itrs itrs = null;
    ...
        
}

其中數組對象 items 存儲說所有數據, ReentrantLock類型成員 lock控制線程并發。notEmpty成員來存放或喚醒被阻塞的消費線程,當數組對象items有元素時,告訴take線程可以執行刪除操作。notFull成員來存放或喚醒被阻塞的生產線程,當隊列未滿時,高速線程可以執行添加元素操作。

非阻塞式添加元素:add、offer方法

非阻塞式添加元素的方法會立即返回,所以其執行線程不會被阻塞。

add方法

add方法調用了父類的add方法,實現如下。

public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}

可以看到add方法是調用offer方法實現的。

offer方法

offer方法流程如下:

  1. 如果數組滿了,就直接釋放鎖,然后返回false。
  2. 如果數組沒滿,就將元素入隊(加入數組),然后返回true。
public boolean offer(E e) {
    checkNotNull(e); // 檢查元素是否為null
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加鎖
    try {
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

我們注意到offer方法調用了enqueue方法。

enqueue方法
// 入隊操作
 private void enqueue(E x) {
    // 獲取當前數組
    final Object[] items = this.items;
    // 賦值
    items[putIndex] = x;
    // 索引自增,如果已經是最后一個位置,重新設置putIndex = 0
    if (++putIndex == items.length)
        putIndex = 0;
    // 隊列中元素數量加1
    count++;
    // 喚醒調用take方法的線程,執行元素獲取操作
    notEmpty.signal();
}

阻塞式添加元素:put方法

阻塞式添加元素時,在隊列滿而不能添加元素時,執行添加操作的線程會被阻塞。

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 該方法可中斷
    try {
        // 當隊列元素個數與數組長度相等時,無法添加元素
        while (count == items.length)
            // 將當前調用線程掛起,添加到notFull隊列條件中,等待被喚醒
            notFull.await();
        enqueue(e); // 如果隊列沒有滿,就直接添加
    } finally {
        lock.unlock();
    }
}

put方法的添加操作流程:

  1. 獲取lock鎖。
  2. 如果隊列已滿,就被阻塞,put線程進入notFull的等待隊列中,等待被喚醒。
  3. 如果隊列未滿,元素通過enqueue方法入隊。
  4. 釋放lock鎖。

非阻塞式刪除元素:poll方法

在隊列空而不能刪除元素時,非阻塞式刪除元素的方法會立即返回,所以其執行線程不會被阻塞。

poll方法
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}

poll 方法刪除此隊列的頭元素,若隊列為空,則立即返回null。poll方法調用了dequeue元素出隊方法。

dequeue方法
// 刪除隊列頭元素
private E dequeue() {
    // 拿到當前數組的數據
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    // 獲取要刪除的對象
    E x = (E) items[takeIndex];
    // 清空位置,將數組中的takeIndex索引位置設置為null
    items[takeIndex] = null;
    // takeIndex索引加1并判斷是否與數組長度相等
    // 如果相等就說明已到盡頭,恢復為0
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--; // 元素減1
    if (itrs != null)
        itrs.elementDequeued(); // 同時更新迭代器中的元素數據
    // 刪除了元素說明隊列有空位,喚醒notFUll條件等待隊列中的put線程,執行添加操作
    notFull.signal();
    return x;
}

阻塞式刪除元素:take方法

take方法是一個可阻塞、可中斷的刪除方法,主要做兩件事:

  1. 如果隊列沒有數據,就將線程加入notEmpty等待隊列并阻塞線程,一直到有元素插入數據并通過notEmpty發送一個消息,notEmpty將從等待隊列喚醒一個消費節點,同時啟動消費線程。

  2. 如果隊列有數據,就通過dequeue方法執行元素的刪除操作。

// 從隊列頭部移除元素,隊列沒有元素就阻塞,可中斷
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 中斷
    try {
        // 如果隊列沒有元素
        while (count == 0)
            // 執行阻塞操作
            notEmpty.await();
        return dequeue(); // 如果隊列有元素就執行刪除操作
    } finally {
        lock.unlock();
    }
}

peek方法返回隊列頭元素

peek方法從takeIndex直接可以獲取最早被添加的元素,不存在就返回null。

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return itemAt(takeIndex); // null when queue is empty
    } finally {
        lock.unlock();
    }
}

ConcurrentHashMap

ConcurrentHashMap 是一個常用的高并發容器類,也是一種線程安全的哈希表。

HashMap 和 HashTable 的問題

基礎容器 HashMap 是線程不安全的,在多線程環境下,使用HashMap進行put操作時,可能會引起死循環,導致CPU利用率飆升。

HashTable是JDK提供的線程安全的Map,它在HashMap基礎上有兩點改進:

  • HashTable不允許key和value為null。
  • HashTable使用synchronized來保證線程安全,所有相關需要同步執行的方法都要加上synchronized關鍵字,對Hash表進行行鎖定。

Hashtable線程安全代價非常大,相當于給整個哈希表加了一把鎖,效率很低。

JDK 1.7中的ConcurrentHashMap

在JDK1.7中ConcurrentHashMap采用了數組+Segment+分段鎖的方式實現。分段鎖并不是鎖,而是一種鎖的設計,用來提升并發線程性能的重要手段,好玩 LongAddr 一樣,所以熱點分散型的削峰手段。

ConcurrentHashMap中的分段鎖稱為Segment,它即類似于HashMap的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表,同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

ConcurrentHashMap使用分段鎖技術,將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問,能夠實現真正的并發訪問。如下圖是ConcurrentHashMap的內部結構圖:

concurrentHashMap_jdk17.png

JDK1.8中的ConcurrentHashMap

JDK1.8中ConcurrentHashMap參考了JDK8 HashMap的實現,采用了數組+鏈表+紅黑樹的實現方式來設計,內部大量采用CAS操作。并發控制使synchronized 和 CAS 來操作。

相比JDK1.7默認情況下將一個table分裂成16個Segement,JDK1.8直接將并發粒度細化到每一個桶,不再需要Segemnt。

JDK1.8中的ConcurrentHashMap內部結構

concurrentHashMap_jdk18.png

其中主要包含如下結構:

  • Node,這是currentHashMap的核心內部類,它包裝了“Key-Value對”。
  • TreeBin, Node子類,當數據鏈表(鏈式桶)長度大于8時,會轉換為TreeBin(樹狀桶),TreeBin作為根節點,可以認為時紅黑樹對象。在ConcurrentHashMap的table“數組”中,存放的就是TreeBin對象,而不是TreeNode對象。
  • TreeNode,樹狀桶的節點類。

JDK1.8中的ConcurrentHashMap主要成員

JDK1.8中的ConcurrentHashMap通過一個Node<K,V>[]數組table來保存添加到哈希表中的桶,而在每一個桶位置是通過鏈表和紅黑樹的形式來保存的。數組table是懶加載的,只有第一次添加元素的時候才會初始化。

ConcurrentHashMap主要成員大致如下:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

    private static final int MAXIMUM_CAPACITY = 1 << 30;
    private static final int DEFAULT_CAPACITY = 16;
   
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;

    =
     /*
     * Encodings for Node hash fields. See above for explanation.
     */
    static final int MOVED     = -1; // 表示正在轉移
    static final int TREEBIN   = -2; // 表示已經轉換成樹
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
    // 數組,用來保存元素
    transient volatile Node<K,V>[] table;
    // 轉移時用的數組
    private transient volatile Node<K,V>[] table;
    // 用來控制表初始化和擴容的控制屬性
    private transient volatile int sizeCtl;

    ...
}

重要屬性如下:

  • table,用于保存添加到哈希表中的桶。
  • DEFAULT_CAPACITY,table的默認長度,默認初始長度是16,即在第一次添加元素時,會將table初始化為16個元素的數組。
  • MIN_TREEIFY_CAPACITY,鏈式桶轉換為紅黑樹的閾值,當鏈表長度大于該值是,將鏈表轉換為紅黑樹。
  • UNTREEIFY_THRESHOLD,紅黑樹桶還原回鏈式桶的閾值,當紅黑樹內節點數量小于6時,將紅黑樹轉換成鏈表。
  • MIN_TREEIFY_CAPACITY,鏈式桶轉換為紅黑樹桶還有一個要求,table的容量達到最小樹形化容量的閾值,只有當哈希表中的table容量大于該值時,才允許將鏈表轉換成紅黑樹的操作。
  • sizeCtl,用來控制table的初始化和擴容操作的過程,其值大致如下:
    • -1代表table正在初始化,其它線程應該交出CPU時間片。
    • -N表示有N-1個線程正在進行擴容操作,嚴格來說,當其為負數時,只用到其低16位,如果其低16位數值為M,此時有M-1個線程進行擴容。
    • 大于0分兩種情況:如果table未初始化,sizeCtl表示table需要初始化的大小;如果table初始化完成,sizeCtl表示table的容量,默認是table大小的0.75倍。

涉及修改sizeCtl的方法有5個:

  • initTable,初始化哈希表。
  • addCount,增加容量。
  • tryPresize,擴容方法之一。
  • transfer,數據轉移到nextTable。
  • helpTransfer,并發添加元素時,如果正在擴容,其它線程會幫助擴容,也就是多線程擴容。

JDK1.8中的ConcurrentHashMap核心源碼

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();
    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)
            // 第一次添加,先初始化node數組
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 計算出table[i]無節點,創建節點
            // 使用Unsafe.compareAndSwapObject原子操作table[i]的位置
            // 如果為null,就添加新建的node節點,跳出循環
            // 反之,再循環進入執行添加操作
            if (casTabAt(tab, i, null,
                            new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            // 如果當前處于轉移狀態,返回新的tab內部表,然后進入循環執行添加操作
            tab = helpTransfer(tab, f);
        else {
            // 在鏈表或紅黑樹中追加節點
            V oldVal = null;
            // 使用synchronized對f對象加鎖
            // 在爭用不激烈的場景中,synchronized和ReetrantLock性能不相上下
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 在鏈表上追加節點
                    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;
                            }
                        }
                    }
                    // 在紅黑樹上追加節點
                    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;
                        }
                    }
                }
            }
            if (binCount != 0) {
                // 節點數大于臨界值,轉換成紅黑樹
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

從put源碼可以發現,JDK1.8版本在使用CAS自旋完成桶的設置時,使用synchronized內置鎖保證桶內并發操作的線程安全。盡管對同一個Map操作的線程爭用會非常激烈,但是在同一個桶內的線程爭用通常不會很激烈,所以使用CAS自旋、synchronized偏向鎖不降低性能。

不使用ReentrantLock是因為為每個桶都創建一個ReentrantLock實例會帶來大量的內存消耗。

get方法源碼如下:

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) {
        if ((eh = e.hash) == h) {
            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;
}

其中并不涉及加鎖操作。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容