CopyOnWriteArrayList分析——能解决什么问题

CopyOnWriteArrayList主要能够解决的问题是并发遍历读取无锁(经过Iterator)数组

对比CopyOnWriteArrayList和ArrayList安全

假如咱们频繁的读取一个可能会变化的清单(数组),你会怎么作?数据结构

一个全局的ArrayList(数组),修改时加锁,读取时加锁多线程

读取时为何须要加锁呢?并发

若是是ArrayList遍历读取时不加锁,这时其余线程修改了ArrayList(增长或删除),会抛出ConcurrentModificationException,这就是failfast机制(咱们这里只讨论Iterator遍历,若是是普通for循环可能会数组越界,这里不讨论)dom

若是是数组遍历读取时,可能会出现数组越界高并发

因此读锁的是写的操做性能

若是读加上锁,那么对于并发读来讲无疑性能是很糟糕的,固然若是你说用读写锁能够解决这个问题,可是咱们这里更期待的是一个无锁的读操做而且能保证线程安全。this

下面这个例子营造的背景是相对高并发的读取+相对低并发的修改spa

List<Integer> arr = new CopyOnWriteArrayList<>();
//List<Integer> arr = new ArrayList<>();//若是经过ArrayList是会报错的
for (int i = 0; i < 3; i++) {
    arr.add(i);
}
//多线程读
for (int i = 0; i < 1000; i++) {
    final int m = i;
    new Thread(() -> {
        try {Thread.sleep(1);} catch (InterruptedException e) {}//等等下面写线程的开始
        Iterator<Integer> iterator = arr.iterator();
        try {Thread.sleep(new Random().nextInt(10));} catch (InterruptedExcep{}//形成不一致的可能性
        int count = 0;
        while(iterator.hasNext()){
            iterator.next();
            count++;
        }
        System.out.println("read:"+count);
    }).start();
}
//多线程写
for (int ii = 0; ii < 10; ii++) {
    new Thread(() -> {
        arr.add(123);
        System.out.println("write");
    }).start();
}

上面的例子若是更换成ArrayList会报错,缘由是:

由于next()方法会调用checkForComodification校验,发现modCount(原始arrayList)与expectedModCount不一致了,这就是上面提到的快速失败,这个快速失败的意思是不管当前是否有并发的状况或问题,只要发现了不一致就抛异常

对于ArrayList解决方案就是遍历iterator时加锁

final void checkForComodification() {
  if (modCount != expectedModCount)
     throw new ConcurrentModificationException();
   }

那么为何换成CopyOnWriteArrayList就能够了呢?咱们先不看CopyOnWrite,咱们先来分析一下CopyOnWriteArrayList的iterator

public Iterator<E> iterator() {
     return new COWIterator<E>(getArray(), 0);
}

CopyOnWriteArrayList 调用iterator时生成的是一个新的数组快照,遍历时读取的是快照,因此永远不会报错(即便读取后修改了列表),而且在CopyOnWriteArrayList是没有fastfail机制的,缘由就在于Iterator的快照实现以及CopyOnWrite已经不须要经过fastfail来保证集合的正确性

CopyOnWriteArrayList的CopyOnWrite即修改数组集合时,会从新建立一个数组并对新数据进行调整,调整完成后将新的数组赋值给老的数组

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);
        newElements[len] = e;//调整新数组
        setArray(newElements);//将新的数组赋值给原数组
        return true;
    } finally {
        lock.unlock();
    }
}

为何要拷贝新的数组,这样作有什么好处?

若是不拷贝新的数组(加锁仍保证其线程安全)直接修改原来的数据结构,那么在读的时候就要加锁了,若是读不加锁就有可能读到修改数组的“半成品”(有可能COWIterator<E>(getArray(), 0);就是个半成品)

而拷贝了新的数组,即便修改没有完成,遍历是拿到的也是老的数组,因此不会有问题。

Doug Lea大神在开发这个类的时候也介绍了这个类的主要应用场景是避免对集合的iterator方法加锁遍历,咱们来看一下这个类的注释的节选:

* making a fresh copy of the underlying array.This is ordinarily too costly, but may be more efficient
* than alternatives when traversal operations vastly outnumber
* mutations, and is useful when you cannot or don't want to
* synchronize traversals, yet need to preclude interference among
* concurrent threads.
* This array never changes during the lifetime of the
* iterator, so interference is impossible and the iterator is
* guaranteed not to throw {@code ConcurrentModificationException}.
* The iterator will not reflect additions, removals, or changes to
* the list since the iterator was created.

大概翻译一下:

拷贝一个新的数组这看上去太昂贵了,可是遍历数远远超过变动数时却十分有效,而且在你不想使用synchronized遍历时会更有用

这份新拷贝的数组在iterator生命周期永远不会改变,而且在迭代是不会让生ConcurrentModificationException异常

一旦迭代器建立,则迭代器不可以被修改(添加、删除元素)

咱们提取一下做者的思想:

一、这个类使用是线程安全的

二、并发经过迭代器遍历不会报错而且无锁

三、在写少读多的前提下,比较合适

相关文章
相关标签/搜索