14個Java併發容器,你用過幾個?

做者:acupt

前言

不考慮多線程併發的狀況下,容器類通常使用ArrayList、HashMap等線程不安全的類,效率更高。在併發場景下,常會用到ConcurrentHashMap、ArrayBlockingQueue等線程安全的容器類,雖然犧牲了一些效率,但卻獲得了安全。
上面提到的線程安全容器都在java.util.concurrent包下,這個包下併發容器很多,今天所有翻出來鼓搗一下。
僅作簡單介紹,後續再分別深刻探索。

併發容器介紹

  1. ConcurrentHashMap:併發版HashMap
  2. CopyOnWriteArrayList:併發版ArrayList
  3. CopyOnWriteArraySet:併發Set
  4. ConcurrentLinkedQueue:併發隊列(基於鏈表)
  5. ConcurrentLinkedDeque:併發隊列(基於雙向鏈表)
  6. ConcurrentSkipListMap:基於跳錶的併發Map
  7. ConcurrentSkipListSet:基於跳錶的併發Set
  8. ArrayBlockingQueue:阻塞隊列(基於數組)
  9. LinkedBlockingQueue:阻塞隊列(基於鏈表)
  10. LinkedBlockingDeque:阻塞隊列(基於雙向鏈表)
  11. PriorityBlockingQueue:線程安全的優先隊列
  12. SynchronousQueue:讀寫成對的隊列
  13. LinkedTransferQueue:基於鏈表的數據交換隊列
  14. DelayQueue:延時隊列

1.ConcurrentHashMap 併發版HashMap

最多見的併發容器之一,能夠用做併發場景下的緩存。底層依然是哈希表,但在JAVA 8中有了不小的改變,而JAVA 7和JAVA 8都是用的比較多的版本,所以常常會將這兩個版本的實現方式作一些比較(好比面試中)。
一個比較大的差別就是,JAVA 7中採用分段鎖來減小鎖的競爭,JAVA 8中放棄了分段鎖,採用CAS(一種樂觀鎖),同時爲了防止哈希衝突嚴重時退化成鏈表(衝突時會在該位置生成一個鏈表,哈希值相同的對象就鏈在一塊兒),會在鏈表長度達到閾值(8)後轉換成紅黑樹(比起鏈表,樹的查詢效率更穩定)。

2.CopyOnWriteArrayList 併發版ArrayList

併發版ArrayList,底層結構也是數組,和ArrayList不一樣之處在於:當新增和刪除元素時會建立一個新的數組,在新的數組中增長或者排除指定對象,最後用新增數組替換原來的數組。
適用場景:因爲讀操做不加鎖,寫(增、刪、改)操做加鎖,所以適用於讀多寫少的場景。
侷限:因爲讀的時候不會加鎖(讀的效率高,就和普通ArrayList同樣),讀取的當前副本,所以可能讀取到髒數據。若是介意,建議不用。
看看源碼感覺下:
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;
    // 添加元素,有鎖
    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(); // 解鎖
        }
    }
    // 讀元素,不加鎖,所以可能讀取到舊數據
    public E get(int index) {
        return get(getArray(), index);
    }
}

複製代碼

3.CopyOnWriteArraySet 併發Set

基於CopyOnWriteArrayList實現(內含一個CopyOnWriteArrayList成員變量),也就是說底層是一個數組,意味着每次add都要遍歷整個集合才能知道是否存在,不存在時須要插入(加鎖)。
適用場景:在CopyOnWriteArrayList適用場景下加一個,集合別太大(所有遍歷傷不起)。

4.ConcurrentLinkedQueue 併發隊列(基於鏈表)

基於鏈表實現的併發隊列,使用樂觀鎖(CAS)保證線程安全。由於數據結構是鏈表,因此理論上是沒有隊列大小限制的,也就是說添加數據必定能成功。

5.ConcurrentLinkedDeque 併發隊列(基於雙向鏈表)

基於雙向鏈表實現的併發隊列,能夠分別對頭尾進行操做,所以除了先進先出(FIFO),也能夠先進後出(FILO),固然先進後出的話應該叫它棧了。

6.ConcurrentSkipListMap 基於跳錶的併發Map

SkipList即跳錶,跳錶是一種空間換時間的數據結構,經過冗餘數據,將鏈表一層一層索引,達到相似二分查找的效果

7.ConcurrentSkipListSet 基於跳錶的併發Set

相似HashSet和HashMap的關係,ConcurrentSkipListSet裏面就是一個ConcurrentSkipListMap,就不細說了。

8.ArrayBlockingQueue 阻塞隊列(基於數組)

基於數組實現的可阻塞隊列,構造時必須制定數組大小,往裏面放東西時若是數組滿了便會阻塞直到有位置(也支持直接返回和超時等待),經過一個鎖ReentrantLock保證線程安全。
用offer操做舉個例子:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    /**
     * 讀寫共用此鎖,線程間經過下面兩個Condition通訊
     * 這兩個Condition和lock有緊密聯繫(就是lock的方法生成的)
     * 相似Object的wait/notify
     */
    final ReentrantLock lock;
    /** 隊列不爲空的信號,取數據的線程須要關注 */
    private final Condition notEmpty;
    /** 隊列沒滿的信號,寫數據的線程須要關注 */
    private final Condition notFull;
    // 一直阻塞直到有東西能夠拿出來
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    // 在尾部插入一個元素,隊列已滿時等待指定時間,若是仍是不能插入則返回
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); // 鎖住
        try {
            // 循環等待直到隊列有空閒
            while (count == items.length) {
                if (nanos <= 0)
                    return false;// 等待超時,返回
                // 暫時放出鎖,等待一段時間(可能被提早喚醒並搶到鎖,因此須要循環判斷條件)
                // 這段時間可能其餘線程取走了元素,這樣就有機會插入了
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);//插入一個元素
            return true;
        } finally {
            lock.unlock(); //解鎖
        }
    }

複製代碼
乍一看會有點疑惑,讀和寫都是同一個鎖,那要是空的時候正好一個讀線程來了不會一直阻塞嗎?
答案就在notEmpty、notFull裏,這兩個出自lock的小東西讓鎖有了相似synchronized + wait + notify的功能。

9.LinkedBlockingQueue 阻塞隊列(基於鏈表)

基於鏈表實現的阻塞隊列,想比與不阻塞的ConcurrentLinkedQueue,它多了一個容量限制,若是不設置默認爲int最大值。

10.LinkedBlockingDeque 阻塞隊列(基於雙向鏈表)

相似LinkedBlockingQueue,但提供了雙向鏈表特有的操做。

11.PriorityBlockingQueue 線程安全的優先隊列

構造時能夠傳入一個比較器,能夠看作放進去的元素會被排序,而後讀取的時候按順序消費。某些低優先級的元素可能長期沒法被消費,由於不斷有更高優先級的元素進來。

12.SynchronousQueue 數據同步交換的隊列

一個虛假的隊列,由於它實際上沒有真正用於存儲元素的空間,每一個插入操做都必須有對應的取出操做,沒取出時沒法繼續放入。
一個簡單的例子感覺一下:
import java.util.concurrent.*;
public class Main {
    public static void main(String[] args) {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                // 沒有休息,瘋狂寫入
                for (int i = 0; ; i++) {
                    System.out.println("放入: " + i);
                    queue.put(i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            try {
                // 鹹魚模式取數據
                while (true) {
                    System.out.println("取出: " + queue.take());
                    Thread.sleep((long) (Math.random() * 2000));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
/* 輸出:
放入: 0
取出: 0
放入: 1
取出: 1
放入: 2
取出: 2
放入: 3
取出: 3
*/

複製代碼
能夠看到,寫入的線程沒有任何sleep,能夠說是全力往隊列放東西,而讀取的線程又很不積極,讀一個又sleep一會。輸出的結果倒是讀寫操做成對出現。
JAVA中一個使用場景就是Executors.newCachedThreadPool(),建立一個緩存線程池。
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
        0, // 核心線程爲0,沒用的線程都被無情拋棄
        Integer.MAX_VALUE, // 最大線程數理論上是無限了,還沒到這個值機器資源就被掏空了
        60L, TimeUnit.SECONDS, // 閒置線程60秒後銷燬
        new SynchronousQueue<Runnable>()); // offer時若是沒有空閒線程取出任務,則會失敗,線程池就會新建一個線程
}

複製代碼

13.LinkedTransferQueue 基於鏈表的數據交換隊列

實現了接口TransferQueue,經過transfer方法放入元素時,若是發現有線程在阻塞在取元素,會直接把這個元素給等待線程。若是沒有人等着消費,那麼會把這個元素放到隊列尾部,而且此方法阻塞直到有人讀取這個元素。和SynchronousQueue有點像,但比它更強大。

14.DelayQueue 延時隊列

可使放入隊列的元素在指定的延時後才被消費者取出,元素須要實現Delayed接口。

總結

上面簡單介紹了JAVA併發包下的一些容器類,知道有這些東西,遇到合適的場景時就能想起有個現成的東西能夠用了。想要知其因此然,後續還得再深刻探索一番。

最後

歡迎你們關注個人公衆號【程序員追風】,文章都會在裏面更新,整理的資料也會放在裏面。
java

相關文章
相關標籤/搜索