并发容器——CopyOnWriteArrayList

概述

对于JAVA并发并发编程我们常需要了解并发底层原理、同步组件、同步集合、线程池还有就是并发容器了,对于并发容器今天讲解下CopyOnWriteArrayList这个替代List的并发集合

CopyOnWriteArrayList类重要成员结构图如下:

CopyOnWriteArrayList
其中lock成员变量便是为什么CopyOnWriteArrayList这个集合在多线程下任然能正确的运行的关键;array这个成员变量就是CopyOnWriteArrayList底层用于封装数据的结构,可以理解为CopyOnWriteArrayList就是一个被改造过了的数组。知道这个两个成员变量可以有效的理解CopyOnWriteArrayList的底层原理。

方法解析

1)add(E e)添加元素到末尾

public boolean add(E e) {
        final ReentrantLock lock = this.lock;//添加元素前获取重入锁
        lock.lock();//加锁
        try {
            Object[] elements = getArray();//获取以前的数组给elements
            int len = elements.length;//获取原数组的长度
            //将原数组内容复制到一个长度比以前多一个长度的新数组
            //这里是关键,解释了CopyOnWriteArrayList的本质“写时复制”
            //即添加元素时会复制出一个新数组,在新数组上添加元素,而不是直接改动原数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
           //这里就解释了,添加元素的时候是在新数组上改动,不改动原数组
            newElements[len] = e;
            //这里就是把新数组的引用传递给CopyOnWriteArrayList的array这个成员,
            //即现在新数组成为了CopyOnWriteArrayList的底层容器,老数组等待被GC
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();//添加元素后释放锁
        }
    }

总结:可以看出CopyOnWriteArrayList底层用数组装元素,每次添加的时候是直接复制出一个新数组,长度只比以前多一个元素的长度,而且他是通过加锁的方式保证了添加元素在多线程下的正确性;所以也可以看出CopyOnWriteArrayList的改动元素的效率低。

2)add(E e,int index)添加元素到指定位置

public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;//计算添加元素的位置要让原数组后面元素移动的位置
            if (numMoved == 0)//如果移动步数为0则是添加到末尾
                newElements = Arrays.copyOf(elements, len + 1);//那么这里就按add(E e)的方式处理了
            else {
                newElements = new Object[len + 1];//创建新元素长度任然比原来数组多一个元素的长度
                //将原数组插入位置前的元素复制到新数组对应位置
                System.arraycopy(elements, 0, newElements, 0, index);
                //将原数组插入位置后的元素复制到新数组对应位置
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;//在新数组对应的插入位置插入元素
            setArray(newElements);//把新数组引用赋予array
        } finally {
            lock.unlock();
        }
    }

总结:添加元素到指定位置,总的实现思路和添加元素到末尾差不多,唯一的不同就每次复制元素的适合要分两次复制,一次复制前面的,一次复制后面的。

3)remove(int index)删除指定位置元素

public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;//计算删除指定位置元素,后面的元素要移动的位置
            if (numMoved == 0)//如果为0就代表是删除最后元素
                setArray(Arrays.copyOf(elements, len - 1));//那就把原数组前n-1个元素复制到新数组中并把引用赋予array
            else {
                Object[] newElements = new Object[len - 1];//创建一个长度减1的新数组
                System.arraycopy(elements, 0, newElements, 0, index);//复制原数组前index个元素到新数组同样位置
                System.arraycopy(elements, index + 1, newElements, index,//复制原数组后index+个元素到新数组同样位置
                                 numMoved);
                setArray(newElements);//改变array的引用赋予CopyOnWriteArrayList的成员变量,老数组等待GC
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

总结:删除元素,大体实现思路和添加等方法的实现差不多,不同的就是分两次复制原数组删除位置的前面的元素到新数组,复制删除元素位置后面的元素到新数组。

总结:从CopyOnWriteArrayList的源码可以看出,添加、删除等修改元素的操作都是通过新建数组在新数组上操作 ,最后把新数组的赋予CopyOnWriteArrayList的array。其中,修改操作都加了锁,所以修改操作是安全的,但效率不高

4)get(int index)获取指定位置的元素

public E get(int index) {
        return get(getArray(), index);//获取元素是从原数组中指定位置获取,
        //这也是为什么CopyOnWriteArrayList并发时//读的效率很高且没有安全,
        //因为没加锁,所以高,因为只是读操作所以是安全的
    }

CopyOnWriteArrayList的弱应用迭代器

特点:都知道一般的集合在迭代的过程中,如果同时在改变集合中的元素,那么会报并发异常,而这底层的原理是JDK的设计者在设计集合的适合,考虑到了这一点,但却不想用加锁的方式来保证其安全,所以采用了fail-fast的机制。具体的原理待下一篇博客
而CopyOnWriteArrayList在迭代的时候如果有元素在改变集合的元素,那么不会报并发错误,同时修改的元素也不会再迭代中体现。具体的原理看下面的源码分析

//COWIterator这个就是我们常说的Iterator
static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array */
        private final Object[] snapshot;//这个是原数组的快照
        /** Index of element to be returned by subsequent call to next.  */
        private int cursor;//数组中元素下标

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        @SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code remove}
         *         is not supported by this iterator.
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }

总结:可以从源码中看出,之所以不会再迭代的时候改变不会报错也不会在迭代过程中体现,是因为CopyOnWriteArrayList在迭代的时候会获取原数组的快照(可以理解为一个复制版本),之后的迭代都是对这个复制版进行迭代的,改变的时候是改变的不同这个快照的数组,相互不影响。

总结

1)CopyOnWriteArrayList适合读操作多余写操作的情况,读操作效率高,写操作效率低
2)CopyOnWriteArrayList的迭代器是若引用,在迭代中改变集合元素时不报错

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值