沉默的背影 X-Pacific

keep learning

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

CopyOnWriteArrayList主要可以解决的问题是并发遍历读取无锁(通过Iterator)

对比CopyOnWriteArrayList和ArrayList

假如我们频繁的读取一个可能会变化的清单(数组),你会怎么做?

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

读取时为什么需要加锁呢?

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

如果是数组遍历读取时,可能会出现数组越界

所以读锁的是写的操作

如果读加上锁,那么对于并发读来说无疑性能是很糟糕的,当然如果你说用读写锁可以解决这个问题,但是我们这里更期待的是一个无锁的读操作并且能保证线程安全。

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

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异常

一旦迭代器创建,则迭代器不能够被修改(添加、删除元素)

我们提取一下作者的思想:

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

2、并发通过迭代器遍历不会报错并且无锁

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

posted @ 2019-05-27 16:11  乂墨EMO  阅读(771)  评论(0编辑  收藏  举报