Java并发多线程编程——并发容器CopyOnWriteArrayList

Posted 小志的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发多线程编程——并发容器CopyOnWriteArrayList相关的知识,希望对你有一定的参考价值。

一、CopyOnWriteArrayList的理解

  • CopyOnWriteArrayList属于java.util.concurrent包;
  • CopyOnWriteArrayList使用到了Copy-On-Write思想,即写入时复制的容器
  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
  • CopyOnWriteArrayList是ArrayList的一种线程安全的变体,在add、set、remove等操作会改变其内部值和长度的时候会通过创建一个新的数组来进行实现。
  • CopyOnWriteArrayList 读的时候不加锁,只有在写的时候才加锁,适用于读操作远远大于写操作。
  • 下图为jdk1.8API的解释:
    在这里插入图片描述

二、CopyOnWriteArrayList的类图

在这里插入图片描述

  • 由类图可知CopyOnWriteArrayList实现了List接口,List继承Collection接口,Collection继承Iterable接口,因此CopyOnWriteArrayList于List使用方法一致;

三、CopyOnWriteArrayList类中常用的方法

1、构造方法

  • CopyOnWriteArrayList() 创建一个空列表。
  • CopyOnWriteArrayList(Collection<? extends E> c) 按照集合的迭代器返回的顺序创建一个包含指定集合元素的列表。
  • CopyOnWriteArrayList(E[] toCopyIn) 创建一个包含给定数组的副本的列表。

2、常用方法

  • public boolean add(E e) 将指定的元素追加到此列表的末尾。
  • public boolean addAll(Collection<? extends E> c) 按照指定集合的迭代器返回的顺序将指定集合中的所有元素追加到此列表的末尾。
  • public E get(int index) 返回此列表中指定位置的元素。
  • public E set(int index, E element) 用指定的元素替换此列表中指定位置的元素。
  • public E remove(int index) 删除该列表中指定位置的元素。
  • public boolean removeAll(Collection<?> c) 从此列表中删除指定集合中包含的所有元素。
  • public Iterator iterator() 以正确的顺序返回该列表中的元素的迭代器。

四、CopyOnWriteArrayList常用方法的原理

1、 add(E e)方法

 /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
    	//先加锁,因为ReentrantLock是独占锁,只有一个线程可以访问
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //拷贝一个新的数组,此时数据的长度是被拷贝数组长度+1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //赋值
            newElements[len] = e;
            //将新的数组赋值给array
            setArray(newElements);
            return true;
        } finally {
            //释放锁
            lock.unlock();
        }
    }
    
     /**
     * Sets the array.
     * 将新的数组赋值给array
     */
    final void setArray(Object[] a) {
        array = a;
    }

2、get(int index)方法

 /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
     /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

3、set(int index, E element)方法

/**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        //先加锁,因为ReentrantLock是独占锁,只有一个线程可以访问
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //获取到数组中的元素
            Object[] elements = getArray();
            //获取到原来的数组中的元素值
            E oldValue = get(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 oldValue;
        } finally {
            lock.unlock();
        }
    }

4、remove(int index) 方法

/**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).  Returns the element that was removed from the list.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
       //先加锁,因为ReentrantLock是独占锁,只有一个线程可以访问
        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)//如果是最后个位置
                //拷贝一个新的数组,此时数据的长度是被拷贝数组长度-1,
                //并将新的数组赋值给array
                setArray(Arrays.copyOf(elements, len - 1));
            else {//如果不是最后个位置,先进行拷贝,在赋值
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            //释放锁
            lock.unlock();
        }
    }

五、结论

  • 由第4步的方法源码解析可知:
  • CopyOnWriteArrayList读取时不加锁,只是写入、删除、修改时加锁,所以合适读数据多写数据少的场景。
  • CopyOnWriteArrayList类中所有写的操作都在新数组中完成,占用内存较高;

以上是关于Java并发多线程编程——并发容器CopyOnWriteArrayList的主要内容,如果未能解决你的问题,请参考以下文章

最全Java并发编程技能:多线程+线程池+线程锁+并发工具+并发容器

Java并发多线程编程——并发容器ConcurrentLinkedQueue

Java并发多线程编程——并发容器CopyOnWriteArrayList

Java并发编程:并发容器(转)

多线程编程-之并发编程:同步容器

Java学习笔记—多线程(同步容器和并发容器)