1. 程式人生 > >高併發第八彈:J.U.C起航(java.util.concurrent)

高併發第八彈:J.U.C起航(java.util.concurrent)

java.util.concurrent是JDK自帶的一個併發的包主要分為以下5部分:

  • 併發工具類(tools)
  • 顯示鎖(locks)
  • 原子變數類(aotmic)
  • 併發集合(collections)
  • Executor執行緒執行器

我們今天就說說 併發集合,除開 Queue,放線上程池的時候講

先介紹以下 CopyOnWrite:

Copy-On-Write簡稱COW,是一種用於程式設計中的優化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容Copy出去形成一個新的內容然後再改,這是一種延時懶惰策略。從JDK1.5開始Java併發包裡提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的併發場景中使用到 .

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

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ 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); //複製當前陣列並且擴容+1 newElements[len] = e; setArray(newElements);//將原來的陣列指向新的陣列 return true; } finally { lock.unlock(); } }

下面這篇文章驗證了CopyOnWriteArrayList和同步容器的效能:

  下面這篇文章簡單描述了CopyOnWriteArrayList的使用:

因為 網友總結的優缺點是:

    • 缺點: 1.寫操作時複製消耗記憶體,如果元素比較多時候,容易導致young gc 和full gc。 2.不能用於實時讀的場景.由於複製和add操作等需要時間,故讀取時可能讀到舊值。 能做到最終一致性,但無法滿足實時性的要求,更適合讀多寫少的場景。 如果無法知道陣列有多大,或者add,set操作有多少,慎用此類,在大量的複製副本的過程中很容易出錯。

    • 設計思想: 1.讀寫分離 2.最終一致性 3.使用時另外開闢空間,防止併發衝突

這個還真是主要是針對 讀多的條件.畢竟寫一個就要開闢一個空間.太耗資源了.其實還是建議用手動的方式來控制集合的併發.

1. ArrayList –> CopyOnWriteArrayList

它相當於執行緒安全的ArrayList。和ArrayList一樣,它是個可變陣列;但是和ArrayList不同的時,它具有以下特性:1. 它最適合於具有以下特徵的應用程式:List 大小通常保持很小,只讀操作遠多於可變操作,需要在遍歷期間防止執行緒間的衝突。2. 它是執行緒安全的。3. 因為通常需要複製整個基礎陣列,所以可變操作(add()、set() 和 remove() 等等)的開銷很大。4. 迭代器支援hasNext(), next()等不可變操作,但不支援可變 remove()等操作。5. 使用迭代器進行遍歷的速度很快,並且不會與其他執行緒發生衝突。在構造迭代器時,迭代器依賴於不變的陣列快照。

 2. HashSet –> CopyOnWriteArraySet

它是執行緒安全的無序的集合,可以將它理解成執行緒安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet雖然都繼承於共同的父類AbstractSet;但是,HashSet是通過“散列表(HashMap)”實現的,而CopyOnWriteArraySet則是通過“動態陣列(CopyOnWriteArrayList)”實現的,並不是散列表。和CopyOnWriteArrayList類似,CopyOnWriteArraySet具有以下特性:1. 它最適合於具有以下特徵的應用程式:Set 大小通常保持很小,只讀操作遠多於可變操作,需要在遍歷期間防止執行緒間的衝突。2. 它是執行緒安全的。3. 因為通常需要複製整個基礎陣列,所以可變操作(add()、set() 和 remove() 等等)的開銷很大。4. 迭代器支援hasNext(), next()等不可變操作,但不支援可變 remove()等 操作。5. 使用迭代器進行遍歷的速度很快,並且不會與其他執行緒發生衝突。在構造迭代器時,迭代器依賴於不變的陣列快照。

SkipList 跳錶:先介紹這個吧

總結起來就是:

  傳統意義的單鏈表是一個線性結構,向有序的連結串列中插入一個節點需要O(n)的時間,查詢操作需要O(n)的時間

  跳錶查詢的複雜度為O(n/2)。跳躍表其實也是一種通過“空間來換取時間”的一個演算法,通過在每個節點中增加了向前的指標,從而提升查詢的效率。

先以資料“7,14,21,32,37,71,85”序列為例,來對跳錶進行簡單說明。

跳錶分為許多層(level),每一層都可以看作是資料的索引,這些索引的意義就是加快跳錶查詢資料速度。每一層的資料都是有序的,上一層資料是下一層資料的子集,並且第一層(level 1)包含了全部的資料;層次越高,跳躍性越大,包含的資料越少。跳錶包含一個表頭,它查詢資料時,是從上往下,從左往右進行查詢。現在“需要找出值為32的節點”為例,來對比說明跳錶和普遍的連結串列。

情況1:連結串列中查詢“32”節點路徑如下圖1-02所示:

需要4步(紅色部分表示路徑)。

情況2:跳錶中查詢“32”節點路徑如下圖1-03所示:

忽略索引垂直線路上路徑的情況下,只需要2步(紅色部分表示路徑)。

先以資料“7,14,21,32,37,71,85”序列為例,來對跳錶進行簡單說明。

跳錶分為許多層(level),每一層都可以看作是資料的索引,這些索引的意義就是加快跳錶查詢資料速度。每一層的資料都是有序的,上一層資料是下一層資料的子集,並且第一層(level 1)包含了全部的資料;層次越高,跳躍性越大,包含的資料越少。跳錶包含一個表頭,它查詢資料時,是從上往下,從左往右進行查詢。現在“需要找出值為32的節點”為例,來對比說明跳錶和普遍的連結串列。

情況1:連結串列中查詢“32”節點路徑如下圖1-02所示:

需要4步(紅色部分表示路徑)。

情況2:跳錶中查詢“32”節點路徑如下圖1-03所示:

忽略索引垂直線路上路徑的情況下,只需要2步(紅色部分表示路徑)。

3. TreeMap –> ConcurrentSkipListMap

下面說說Java中ConcurrentSkipListMap的資料結構。(01) ConcurrentSkipListMap繼承於AbstractMap類,也就意味著它是一個雜湊表。(02) Index是ConcurrentSkipListMap的內部類,它與“跳錶中的索引相對應”。HeadIndex繼承於Index,ConcurrentSkipListMap中含有一個HeadIndex的物件head,head是“跳錶的表頭”。(03) Index是跳錶中的索引,它包含“右索引的指標(right)”,“下索引的指標(down)”和“雜湊表節點node”。node是Node的物件,Node也是ConcurrentSkipListMap中的內部類。

    /**
     * Special value used to identify base-level header
     */
    private static final Object BASE_HEADER = new Object();

    /**
     * 跳錶的最頂層索引
     */
    private transient volatile HeadIndex<K,V> head;

    /**
     * 
      * 比較器用於維護此對映中的順序,或者如果使用自然排序,則為空。(非私有的,以
      * 簡化巢狀類中的訪問)。
     * 
     */
    final Comparator<? super K> comparator;

    /** Lazily initialized key set */ //懶惰初始化金鑰集
    private transient KeySet<K> keySet;
    /** Lazily initialized entry set */
    private transient EntrySet<K,V> entrySet;
    /** Lazily initialized values collection */
    private transient Values<V> values;
    /** Lazily initialized descending key set */    

原始碼我也沒精力去詳勘了.就總結一下

4. TreeSet –> ConcurrentSkipListSet

(01) ConcurrentSkipListSet繼承於AbstractSet。因此,它本質上是一個集合。(02) ConcurrentSkipListSet實現了NavigableSet介面。因此,ConcurrentSkipListSet是一個有序的集合。(03) ConcurrentSkipListSet是通過ConcurrentSkipListMap實現的。它包含一個ConcurrentNavigableMap物件m,而m物件實際上是ConcurrentNavigableMap的實現類ConcurrentSkipListMap的例項。ConcurrentSkipListMap中的元素是key-value鍵值對;而ConcurrentSkipListSet是集合,它只用到了ConcurrentSkipListMap中的key!

(4)同其他set集合,是基於map集合的(基於ConcurrentSkipListMap),在多執行緒環境下,裡面的contains、add、remove操作都是執行緒安全的。

 (5)多個執行緒可以安全的併發的執行插入、移除、和訪問操作。但是對於批量操作addAll、removeAll、retainAll和containsAll並不能保證以原子方式執行,原因是addAll、removeAll、retainAll底層呼叫的還是  contains、add、remove方法,只能保證每一次的執行是原子性的,代表在單一執行操縱時不會被打斷,但是不能保證每一次批量操作都不會被打斷。在使用批量操作時,還是需要手動加上同步操作的。

(6)不允許使用null元素的,它無法可靠的將引數及返回值與不存在的元素區分開來。

5.  HashMap –> ConcurrentHashMap

  • 不允許空值,在實際的應用中除了少數的插入操作和刪除操作外,絕大多數我們使用map都是讀取操作。而且讀操作大多數都是成功的。基於這個前提,它針對讀操作做了大量的優化。因此這個類在高併發環境下有特別好的表現。
  • ConcurrentHashMap作為Concurrent一族,其有著高效地併發操作,相比Hashtable的笨重,ConcurrentHashMap則更勝一籌了。
  • 在1.8版本以前,ConcurrentHashMap採用分段鎖的概念,使鎖更加細化,但是1.8已經改變了這種思路,而是利用CAS+Synchronized來保證併發更新的安全,當然底層採用陣列+連結串列+紅黑樹的儲存結構。
安全共享物件策略
  • 執行緒限制:一個被執行緒限制的物件,由執行緒獨佔,並且只能被佔有它的執行緒修改
  • 共享只讀:一個共享只讀的U帝鄉,在沒有額外同步的情況下,可以被多個執行緒併發訪問,但是任何執行緒都不能修改它
  • 執行緒安全物件:一個執行緒安全的物件或者容器,在內部通過同步機制來保障執行緒安全,多以其他執行緒無需額外的同步就可以通過公共介面隨意訪問他
  • 被守護物件:被守護物件只能通過獲取特定的鎖來訪問。

不好意思 虎頭蛇尾了.實在扛不住了