1. 程式人生 > >並發容器(三)非阻塞隊列的並發容器

並發容器(三)非阻塞隊列的並發容器

接口 index except jdk1 fab lis tarray warning 上進

??本文將介紹除了阻塞隊列外的並發容器: ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue;

1. CopyOnWriteArrayList

  • 是 ArrayList 的線程安全的實現,同時也可用於代替 Vector 。底層實現是一個數組,其中所有可變操作(add、set 等等)都是通過對底層數組進行一次新的復制來實現的。這就是 “寫時復制”。
  • 可變操作一般需要很大的開銷,但是當遍歷操作的數量大大超過可變操作的數量時,這種方法可能比其他替代方法更 有效。在不能或不想進行同步遍歷,但又需要從並發線程中排除沖突時,它也很有用。
  • 叠代器在創建時,使用了數組狀態的快照,此數組快照在叠代期間是不會改變的,因此也不會發生沖突,並且叠代器保證不會拋出 ConcurrentModificationException。
  • 叠代器使用的是數組的快照,所以叠代器是無法反映列表的添加、移除或者更改。
  • 在叠代器上進行的元素更改操作(remove、set 和 add)不受支持。這些方法將拋出 UnsupportedOperationException。

看一下add()方法的源碼:add()方法加了鎖,添加一個元素,就是將數組的元素復制到新的數組中(新數組的大小=舊數組大小+1),再把新元素放到新數組中。remove方法也是如此。還提供原子操作 addIfAbsent() 方法。

 public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        //加鎖
        lock.lock();
        try {
            //獲取當前的數組
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw
new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1);//復制創建新數組 else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element;//添加新元素到新數組中去 setArray(newElements);//將新數組設為 當前對象的底層數組 } finally { lock.unlock(); } }

2. CopyOnWriteArraySet

??CopyOnWriteArraySet 是 HashSet 線程安全的一個實現。CopyOnWriteArraySet 的實現是基於 CopyOnWriteArrayList,其內部維護著一個 CopyOnWriteArrayList。其特性可參考 CopyOnWriteArrayList。

private final CopyOnWriteArrayList<E> al;

/**
     * Creates an empty set.構造方法
     */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

    public int size() {
        return al.size();
    }

    public boolean isEmpty() {
        return al.isEmpty();
    }

    public boolean contains(Object o) {
        return al.contains(o);
    }

    public boolean add(E e) {
        return al.addIfAbsent(e);
    }
    //........

get()方法使用的也是數組的快照,沒有加鎖阻塞,這就意味著get()方法返回的值不是很精確。

 public E get(int index) {
        return get(getArray(), index);
    }

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

3. ConcurrentLinkedQueue

??一個基於鏈接節點的 無界線程安全隊列 。此隊列按照 FIFO(先進先出)原則對元素進行排序。隊列的頭部 是隊列中時間最長的元素。隊列的尾部 是隊列中時間最短的元素。新的元素插入到隊列的尾部,隊列獲取操作從隊列頭部獲得元素。
??ConcurrentLinkedQueue 采用了非阻塞的CAS算法,在高並發的環境下,性能非常好。其源碼分析可參考。

4. ConcurrentHashMap

??ConcurrentHashMap 是 HashMap 線程安全的實現,同時也用於 代替 HashTable。(此類可以通過程序完全與 Hashtable 進行互操作,這取決於其線程安全,而與其同步細節無關。)。不同於HashTable(一張hash表只用一把鎖),在ConcurrentHashMap中,會將hash表的數據分成若幹段,每段維護一個鎖,粒度更細,以達到高效的並發訪問;

??ConcurrentHashMap 與 其他並發容器一樣,在叠代的過程不需要加鎖,叠代器具有弱一致性,叠代期間不會拋出ConcurrentModificationException異常,並非“立即失敗”;所謂 弱一致性 ,就是返回的元素將反映叠代器創建時或創建後某一時刻的映射狀態。同時,需要在整個Map上進行計算的方法,如 size()、isEmpty(),這些方法的語義被略微減弱,以反映並發的特性,換句話說,這些方法的值是一個估計值,並不是很精確。事實上,這些方法在並發環境下用處很小,因為在並發的情況下,它們的返回值總是在變化。如果需要強一致性,那麽就得考慮加鎖。同步容器類便是強一致性的

??由於 ConcurrentHashMap 不能被加鎖來執行獨占訪問,因此無法通過加鎖來創建新的原子操作。不過,ConcurrentHashMap 提供了以下幾個原子操作(由其父接口 ConcurrentMap 提供),基本滿足需求了:

//如果指定鍵已經不再與某個值相關聯,則將它與給定值關聯。
V putIfAbsent(K key, V value);

//只有目前將鍵的條目映射到給定值時,才移除該鍵的條目。
boolean remove(Object key, Object value);

//只有目前將鍵的條目映射到某一值時,才替換該鍵的條目。
V replace(K key, V value);

//只有目前將鍵的條目映射到給定值時,才替換該鍵的條目。
boolean replace(K key,V oldValue, V newValue);

5. ConcurrentSkipListMap

??ConcurrentSkipListMap 是 TreeMap 的線程安全的實現。與上面的並發容器一樣,叠代器是是弱一致性,返回的元素將反映叠代器創建時或創建後某一時刻的映射狀態。它們不 拋出 ConcurrentModificationException,可以並發處理其他操作。size()操作返回的值也不是精確的。此外,批量操作 putAll、equals 和 clear 並不保證能以原子方式 (atomically) 執行 。例如,與 putAll 操作一起並發操作的叠代器只能查看某些附加元素。

6. ConcurrentSkipListSet

?? ConcurrentSkipListSet 是 TreeSet 的線程安全的實現。ConcurrentSkipListSet 是基於 ConcurrentSkipListMap 實現的,就是將所有Map的Key的值所對應的 value 值為Boolean.TRUE。其特性參考 ConcurrentSkipListMap

 private final ConcurrentNavigableMap<E,Object> m;

 public boolean add(E e) {
        return m.putIfAbsent(e, Boolean.TRUE) == null;
    }

7. 最後總結幾點:

??並發容器的性能一般都要比同步容器的性能更高。同步容器的所有公開的方法都用 synchronized 加了鎖,所以同一時間只能一個線程訪問同步容器。並發容器類則是鎖的粒度更小(多個線程就可以並發地訪問方法裏面的非臨界區代碼,提高效率),還有的采用了 非阻塞的算法CAS(如 ConcurrentLinkedQueue),鎖分段等技術。當然,這並不是說同步容器就沒有用了,如希望程序的 HashSet 線程安全,可以采用 CopyOnWriteArraySet,但如果寫操作多於讀操作的話,那麽就應該采用 Collections.synchronizedSet(Set

??並發容器是“弱一致性”,因此在叠代器創建後,不能反映容器的增、刪、改的情況,同時size、isEmpty等方法只能得到一個估值,不是很精確。“弱一致性” 雖然在一些地方做出犧牲(就是上面所說的),但也極大提高了並發容器的其他方面性能,特別是叠代,叠代是可以並發進行,不需要額外的同步,也不會拋出 ConcurrentModificationException。

同步容器是“強一致性”,所以同步容器的叠代操作都需要加鎖來保證原子性操作。

對於除了叠代操作外的復合操作,並發容器中某些類提供了常用的復合操作的原子性方法(如:ConcurrentHashMap.putIfAbsent(K key, V value) )。對於復合操作,要謹慎,特別是同步容器,記得加鎖來保證原子性。


下面是源碼分析的好文章,值得一看

1. 【JUC】JUC集合框架綜述
2. 【JUC】JDK1.8源碼分析之ConcurrentHashMap(一)
3. 【JUC】JDK1.8源碼分析之ConcurrentSkipListMap(二)
4. 【JUC】JDK1.8源碼分析之ArrayBlockingQueue(三)
5. 【JUC】JDK1.8源碼分析之LinkedBlockingQueue(四)
6. 【JUC】JDK1.8源碼分析之ConcurrentLinkedQueue(五)
7. 【JUC】JDK1.8源碼分析之CopyOnWriteArrayList(六)
8. 【JUC】JDK1.8源碼分析之CopyOnWriteArraySet(七)
9.【JUC】JDK1.8源碼分析之ConcurrentSkipListSet(八)
10.【JUC】JDK1.8源碼分析之SynchronousQueue(九)

並發容器(三)非阻塞隊列的並發容器