掌握系列之併發編程-8.併發容器類

掌握高併發、高可用架構

第二課 併發編程

從本課開始學習併發編程的內容。主要介紹併發編程的基礎知識、鎖、內存模型、線程池、各種併發容器的使用。

第八節 併發容器類

併發容器 CAS

借了一張圖,展示了JDK的容器類族譜。

这里写图片描述

Map
    Interface:
        Map
        SortedMap
        NavigableMap
        ConcurrentMap
        ConcurrentNavigableMap
    Class:
        EnumMap
        HashMap
        WeakHashMap
        IdentifyHashMap
        TreeMap
        LinkedHashMap
        ConcurrentHashMap
        ConcurrentSipListMap
Collection
    List
        Interface:
            List
        Class:
            ArrayList
            LinkedList
            CopyOnWriteArrayList
    Queue
        Interface:
            Queue
            Deque
            BlockingQueue
            BlockingDeque
            TransferQueue
        Class:
            ArrayDeque
            PriorityQueue
            ArrayBlockingQueue
            LinkedBlockingQueue
            SynchronousQueue
            PriorityBlockingQueue
            LinkedTransferQueue
            DelayQUeue
            LinkedBlockingDeque
    Set
        Interface:
            Set
            SortedSet
            NavigableSet
        Class:
            HashSet
            TreeSet,可對元素進行排序
            LinkedHashSet,保持了元素的插入順序
            CopyOnWriteArraySet
            ConcurrentSkipListSet
併發List
  1. CopyOnWriteArrayList,是ArrayList的線程安全體,由可變數組來實現。和ArrayList的區別是其數組內部均爲有效數據。

    CopyOnWrite機制的一種容器:當添加一個元素時,不直接添加到當前容器,而是先將當前容器進行復制,複製出一個新的容器,並把元素添加到新容器中,完成之後,再將原容器的引用指向新容器;刪除一個元素,也是同理的。

    優點:可以對CopyOnWrite容器進行併發讀操作,而不加鎖

    缺點:寫操作效率低下,內存佔用率高;做不到實時一致性

    總結,適合讀多寫少的數據

併發Queue

併發的Queue主要有4種,共9個:

  • BlockingQueue,阻塞隊列,包括ArrayBlockingQueueDelayQueueLinkedBlockingQueuePriorityBlockingQueeSynchronousQueue
  • ConcurrentLinkedDeque
  • LinkedBlockingDeque
  • ConcurrentLinkedQueue
  • LinkedTransferQueue

下面挨個來介紹。

  1. ArrayBlockingQueue,是一個數組實現的有界阻塞隊列。內部由一個ReentrantLock來控制生產和消費,兩個Condition(notEmpty和notFull)控制,當取數據時,如果隊列爲空,則notEmpty.await();當添加數據時,如果隊列滿了,則notFull.await()。取出數據後,執行notFull.signal();添加數據後,執行notEmpty.signal()。隊列元素位置計數由takeIndex、putIndex、count控制。默認是非公平鎖的方式訪問隊列,可以指定爲公平鎖。

    public ArrayBlockingQueue(int capacity, boolean fair) {
       if (capacity <= 0)
           throw new IllegalArgumentException();
       this.items = new Object[capacity];
       lock = new ReentrantLock(fair);
       notEmpty = lock.newCondition();
       notFull =  lock.newCondition();
    }
  2. LinkedBlockingQueue,是利用鏈表實現的有界阻塞隊列。如果不指定容量的話,默認的最大長度爲Integer.MAX_VALUE。其內部對生產和消費使用了不同的鎖,ReentrantLock的takeLock和ReentrantLock的putLock。對於putoffer採用同一把鎖,takepoll採用另一把鎖,從而避免讀寫時互相競爭鎖的情況,分離了讀寫線程安全。在高併發讀寫的情況下都比ArrayBlockingQueue效率高。在遍歷刪除時會同時鎖住兩把鎖。

    阻塞由兩個Condition(notEmpty和notFull)控制,隊列元素位置計數由變量AtomicInteger count控制

    put操作時,在putLock鎖內,若隊列滿,則notFull.await(),不滿時,notFull.signal()喚醒。

    take操作時,在takeLock鎖內,若隊列空,則notEmpty.await(),不空時,notEmpty.signal()進行喚醒。

    offer是無阻塞的enqueue或時間範圍內的阻塞enqueue。

    poll是無阻塞的dequeue或時間範圍內的阻塞dequeue。

    static class Node<E> {
       E item;
    
       Node<E> next;
    
       Node(E x) { item = x; }
    }
  3. DelayQueue,是一個支持延時獲取元素的×××阻塞隊列。存放數據 的隊列是PriorityQueue,優先隊列的比較基準值是時間。隊列中的元素必須實現Delayed接口,其擴展了Comparable接口,比較的基準爲延時的時間。Delayed接口的方法getDelayed的返回值爲long。該方法指定元素創建多久才能從隊列中獲取到該元素。

    應用場景可以是:

    • 緩存系統的設計:存儲緩存元素的有效期,當能獲取到元素時表示緩存有效期到了
    • 定時任務的調度:存儲要執行的任務,當能獲取到任務時就可以執行一次
  4. SynchronousQueue,是一個不存儲元素的阻塞隊列。每個put操作必須等待一個take操作,否則不能繼續添加元素。SynchronousQueue可以看成一個傳球手,負責把生產者處理的數據直接傳遞給消費者。

    可以認爲SynchronousQueue是一個緩存值爲1的阻塞隊列,它不能調用peek()來看隊列中是否有元素,因爲只有你來取的時候纔可能存在,不取只偷看是不允許的。遍歷隊列也是不允許的。isEmpty()永遠返回true。remainingCapacity()永遠返回0。remove()removeAll()永遠返回false。iterator()永遠返回null,peek()也是null。

    適合傳遞性場景。其吞吐量高於LinkedBlockingQueueArrayBlockingQueue

  5. PriorityBlockingQueue,是一個按照優先級排列的阻塞隊列,內部維護了一個由數組實現的平衡二叉樹,存儲的元素必須實現Comparable接口,用於判斷元素的優先級。

    添加新元素時,並不是把全部元素進行順序排列,而是從某個位置開始與新元素進行比較,一直比較到隊列頭,保證隊頭一定是優先級最高的元素。

    每取一個頭元素,都會對剩下的元素進行調整,保證隊頭一定是優先級最高的元素

  6. ConcurrentLinkedDeque,源自jdk1.7,是一種非阻塞式併發雙向×××隊列,同時支持FIFO(先進先出)和FILO(先進後出)兩種操作方式

  7. LinkedBlockingDeque,源自jdk1.6,是一種由鏈表實現的阻塞式併發雙向隊列,即可以從隊頭和隊尾同時操作。如果不指定容量,默認爲Integer.MAX_VALUE。

    對於讀寫操作,採用一個獨佔鎖來實現,所有的操作都枷鎖來實現併發。由於是獨佔鎖,不能同時進行兩個操作,所以性能大打折扣。性能順序:ConcurrentLinkedQueue > LinkedBlockingQueue > LinkedBlockingDeque

  8. ConcurrentLinkedQueue,是一種非阻塞式併發鏈表,採用先進先出的規則。使用wait-free算法解決併發問題

  9. LinkedTransferQueue,源自jdk1.7,是一種阻塞隊列,增加了transfer相關的方法。transfer的語義是,生產者會一直阻塞直到transfer到隊列的元素被某個消費者消費(不只是放入隊列,還必須等到被消費)。使用put時不需要等待消費。

    LinkedTransferQueue採用一種預佔模式,即當消費者來獲取元素時,如果隊列爲空,那就生成一個節點(節點元素爲null)入隊,然後消費者被park住。後面的生產者入隊時發現有一個元素爲null的節點,此時生產者不入隊,而是直接將元素填充到該節點,喚醒該節點上park的線程,被喚醒的消費者拿到元素走人。

併發Set
  1. CopyOnWriteArraySet,內部是一個CopyOnWriteArrayList,所有操作都是基於對CopyOnWriteArrayList的。
  2. ConcurrentSkipListSet,內部是一個ConcurrentSkipListMap,Set的數據value被封裝成<value, Boolean.TRUE>放入ConcurrentSkipListMap。需要注意的是value必須實現Comparable接口。
併發Map
  1. ConcurrentHashMap,在jdk1.8之前使用鎖分段技術來實現併發,並且提高了併發訪問效率。鎖分段原理:將數據分成一段一段進行存儲,給每段數據配一把鎖;當一個線程佔有鎖來訪問一個段數據時,其他段的數據也可以被別的線程訪問。

    ConcurrentHashMap實現時,由Segment數組HashEntry數組組成。Segment是一種可重入的ReentranLock,HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap包含一個Segment數組,Segment的數據結構和HashMap類似,是一種數組和鏈表結構,一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,當對數據進行修改時,必須獲得相對應HashEntry的Segment的鎖

    image

    jdk1.8開始,ConcurrentHashMap摒棄了Segment段鎖的概念,啓用CAS算法實現。其底層數據結構和相對應的HashMap類似,採用“數組 + 鏈表 + 紅黑樹”。但是爲了做到併發,又增加了很多輔助的類,如TreeBin、Traverser等內部類。

  2. ConcurrentSkipListMap,是TreeMap的線程安全版本,使用CAS算法實現線程安全,適用於多線程情況下對Map的鍵值進行排序。

    對於鍵值排序需求,非多線程的情況下,應當儘量使用TreeMap;對於併發性相對較低的情況下,可以使用Collections.synchronizedSortedMap將TreeMap進行包裝,也可以提供較好的效率;對於高併發的程序,應當使用ConcurrentSkipListMap,能夠提供更高的併發度。它比ConcurrentHashMap有更高的併發度。ConcurrentSkipListMap的存取時間複雜度是logN,和線程數基本無關。所以在數據量一定的情況下,併發線程越多,越能體現它的優勢。

    ConcurrentSkipListMap是由跳錶(Skip List)實現的,默認是按照Key值升序的,內部是由Node和Index組成的

其他的併發容器類

還有Collections工具類中提供的一系列普通容器類的線程安全版本的包裝類。

Collections.synchronizedCollection(Collection c) -> SynchronizedCollection
Collections.synchronizedList(List l) -> SynchronziedList
Collections.synchronizedMap(Map m) -> SynchronizedMap
Collectionos.synchronizedSet(Set s) -> SynchronizedSet

其效率低於真正的併發容器類

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章