深刻淺出 Java Concurrency (27): 併發容器 part 12 線程安全的List/Set[轉]

本小節是《併發容器》的最後一部分,這一個小節描述的是針對List/Set接口的一個線程版本。html

在《併發隊列與Queue簡介》中介紹了併發容器的一個歸納,主要描述的是Queue的實現。其中特別提到一點LinkedList是List/Queue的實現,可是LinkedList確實非線程安全的。無論BlockingQueue仍是ConcurrentMap的實現,咱們發現都是針對鏈表的實現,固然儘量的使用CAS或者Lock的特性,同時都有經過鎖部分容器來提供併發的特性。而對於List或者Set而言,增、刪操做其實都是針對整個容器,所以每次操做都不可避免的須要鎖定整個容器空間,性能確定會大打折扣。要實現一個線程安全的List/Set,只須要在修改操做的時候進行同步便可,好比使用java.util.Collections.synchronizedList(List<T>)或者java.util.Collections.synchronizedSet(Set<T>)。固然也可使用Lock來實現線程安全的List/Set。java

一般狀況下咱們的高併發都發生在「多讀少寫」的狀況,所以若是可以實現一種更優秀的算法這對生產環境仍是頗有好處的。ReadWriteLock固然是一種實現。CopyOnWriteArrayList/CopyOnWriteArraySet確實另一種思路。算法

CopyOnWriteArrayList/CopyOnWriteArraySet的基本思想是一旦對容器有修改,那麼就「複製」一份新的集合,在新的集合上修改,而後將新集合複製給舊的引用。固然了這部分少不了要加鎖。顯然對於CopyOnWriteArrayList/CopyOnWriteArraySet來講最大的好處就是「讀」操做不須要鎖了。數組

咱們來看看源碼。安全

/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
public E get(int index) {
    return (E)(getArray()[index]);
}
private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
    public void clear() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        setArray(new Object[0]);
    } finally {
        lock.unlock();
    }
    }

對於上述代碼,有幾點說明:併發

  1. List仍然是基於數組的實現,由於只有數組是最快的。
  2. 爲了保證無鎖的讀操做可以看到寫操做的變化,所以數組array是volatile類型的。
  3. get/indexOf/iterator等操做都是無鎖的,同時也能夠看到所操做的都是某一時刻array的鏡像(這得益於數組是不可變化的)
  4. add/set/remove/clear等元素變化的都是須要加鎖的,這裏使用的是ReentrantLock。

這裏有一段有意思的代碼片斷。高併發

    public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        Object oldValue = elements[index];
        if (oldValue != element) {
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len);
        newElements[index] = element;
        setArray(newElements);
        } else {
        // Not quite a no-op; ensures volatile write semantics
        setArray(elements);
        }
        return (E)oldValue;
    } finally {
        lock.unlock();
    }
    }

final void setArray(Object[] a) {
    array = a;
}

對於set操做,若是元素有變化,修改後setArray(newElements);將新數組賦值還好理解。那麼若是一個元素沒有變化,也就是上述代碼的else部分,爲何還須要進行一個無謂的setArray操做?畢竟setArray操做沒有改變任何數據。性能

對於這個問題也是頗有意思,有一封郵件討論了此問題(123)。
大體的意思是,儘管沒有改變任何數據,可是爲了保持「volatile」的語義,任何一個讀操做都應該是一個寫操做的結果,也就是讀操做看到的數據必定是某個寫操做的結果(儘管寫操做沒有改變數據自己)。因此這裏即便不設置也沒有問題,僅僅是爲了一個語義上的補充(我的理解)。ui

這裏還有一個有意思的討論,說什麼addIfAbsent在元素沒有變化的時候爲何沒有setArray操做?這個要看怎麼理解addIfAbsent的語義了。若是說addIfAbsent語義是」寫「或者」不寫「操做,而把」不寫「操做看成一次」讀「操做的話,那麼」讀「操做就不須要保持volatile語義了。this

 

對於CopyOnWriteArraySet而言就簡單多了,只是持有一個CopyOnWriteArrayList,僅僅在add/addAll的時候檢測元素是否存在,若是存在就不加入集合中。

private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

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

 

在使用上CopyOnWriteArrayList/CopyOnWriteArraySet就簡單多了,和List/Set基本相同,這裏就再也不介紹了。

 

整個併發容器結束了,接下來好好規劃下線程池部分,而後進入最後一部分的梳理。

相關文章
相關標籤/搜索