深刻浅出 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基本相同,这里就再也不介绍了。

 

整个并发容器结束了,接下来好好规划下线程池部分,而后进入最后一部分的梳理。

相关文章
相关标签/搜索