并发库 —— CopyOnWriteArrayList

    技术2022-07-17  100

    概述

    CopyOnWrite(COW)是在写操作的时候copy当前数据,然后在写完数据之后设置成新的数据。适用于读多写少的并发场景。

    CopyOnWrite 使用了 ReentrantLock(支持重入的独占锁) 来支持并发操作。ReentrantLock 是一种支持重入的独占锁,任意时刻只允许一个线程来获得锁。ReentrantLock 默认是非公平锁(即不按照进入等待队列的顺序唤醒线程)机制。

    本质是一种延时策略,只有在真正需要复制的时候才复制,而不是提前复制好。

    源码分析

    添加与读取元素 final transient ReentrantLock lock = new ReentrantLock(); final transient volatile Object[] array; 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(); // 必须手动解锁 } } public boolean add (int index, E e){ final ReentrantLock lock = this.lock; lock.lock(); try{ Object[] elements = getArray(); int len = elements.length; if(index > len || index < ){ throw new IndexOutOfBoundsExcption("d--"); } Obejct[] newElements; int numMoved = len - index; if(numMoved == 0){ // 如果插入尾部,则直接进行拷贝 newElements = Arrays.copyOf(elements, len+1); } else { // 如果index位置上已经有了元素,则分两次拷贝 newElements = new Obejct[len+1]; // 将index之前的数据全部拷贝到新数组,不包括index System.arrayCopy(elements, 0, newElements, 0, index); // 将index之后拷贝到新数组,不包括index System.arrayCopy(elements, idnex, newElements, index+1, numMoved); } newElements[index] = e; setArray(newElements); }finally{ lock.unlock(); } } // 读取的时候并没有加同步,因此允许多个线程进入读取 public E get(int index){ return get(getArray(), index); } pulbic E get(Object[] array, int index){ return (E)array[index]; }

    综上:在写入数据时使用ReentrantLock进行加锁,保证每一次写操作都是一个线程完成的,且每一次写操作都会先拷贝一份数据,然后再对新数组进行写操作;在读取数据的时候没有加锁,因此是允许多个线程进入访问的。所以,CopyOnWrite天生 适合读多写少的场景。

    数组的遍历 private final Object[] snapshot; // 数组快照 private int cursor; // 遍历的时候会先获得当前数组对象的一个拷贝,即生成一个快照,接着遍历操作在快照上进行。 public Iterator<E> iterator(){ return COWIterator<E>(getArray(),0); } public void testCOWIterator(){ CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList(); list.add("xiaoming"); list.add("hanmeimei"); Iterator<String> iterator = list.iterator(); list.add("kangkang"); while(iterator.hasNext()){ syso(iterator.next()); } // out xiaoming hanmeimei }

    综上:CopyOnWrite使用迭代器遍历时,我们只是获取当前list的一个快照,在对list获取迭代器后,再对list进行各种操作,是不会影响快照的遍历的,即每个线程都将获得当前数组的一个快照,所以迭代器不需要加锁就可以安全的实现遍历。

    应用场景

    应用于读多写少的并发场景。例如:白名单,黑名单,往往很长时间才会进行修改,大部分情况下我们只需要读数据。

    总结

    CopyOnWrite翻译来讲的话就是写时复制,就是我们往容器添加数据的时候不是直接添加,而是先将当前容器拷贝到新的容器中,然后往新的容器里面添加数据,最后在原容器的引用指向新的容器。因此,在并发读的情况下我们不需要加锁,因为当前的容器并不会被修改。

    CopyOnWrite的优缺点:

    优点:只在并发写的时候才会加锁,访问与读取不需要加锁,性能上比较高缺点: 由于每次写数据时会复制整个数组,若容器对象占用的内存比较大时,容易发生GC。因此如果我们想用这个COW机制的话,数据不能太大,修改机会也比较少;通过上面的源码分析,不难看出COW只能保证数据最终的一致性,不能保证数据的实时一致性。当我们碰到写入的数据需要立马读取到的话,就不要用CWO了。

    最后说一句: CopyOnWriteArrayList 在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果修改非常少,数组量又不大,并且对读性能要求比较高的场景,使用COW容器的效果就比较好了。

    Processed: 0.013, SQL: 9