Java并发集合之CopyOnWriteArrayList使用与原理

CopyOnWriteArrayList简介

CopyOnWriteArrayList是线程安全的可变长度的List集合,相当于线程安全的ArrayList。
线程安全与线程不安全的区别:

  • 线程安全:多个线程同时读写该集合时不会发生ConcurrentModificationException的异常
  • 线程不安全:多个线程同时读写集合是会发生ConcurrentModificationException异常

CopyOnWriteArrayList的特性

  • 适用于数据量较小且读多写少的场景
  • 它是线程安全的
  • 新增和删除等修改数据的操作开销很大,涉及到数组复制后续源码解析会详解
  • 迭代器只支持读取不支持变更,并且在迭代过程中其他线程写入集合不会发生并发冲突

CopyOnWriteArrayList的数据结构

  • CopyOnWriteArrayList底层也是通过数组来实现的,每次新增或删除数据都会通过拷贝数组的方式实现动态变更,源码定义如下
private transient volatile Object[] array;
  • volatile关键字用于在多线程操作中可以拿到最新的数组数据

CopyOnWriteArrayList的部分源码解析

构造方法

  • 无参构造方法,创建一个空数组
final Object[] getArray() {
        return array;
    }
final void setArray(Object[] a) {
        array = a;
    }
public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
  • 有参构造方法,通过数组拷贝将参数中的数组或者集合拷贝到私有属性的array中
public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            if (c.getClass() != ArrayList.class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

变更数据方法

  • add方法
public boolean add(E e) {
		// 获取当前实例的可重入锁
        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;
            // 将当前实例的底层数组指向新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

这里可以解释为什么称为CopyOnWrite的原因,因为在写入数据的时候会创建一个新的数组,将原数组的数据进行拷贝后再替换原数组,这也是在进行新增数据的时候会开销很大的原因

  • set方法
public E set(int index, E element) {
		// 获取锁
        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();
        }
    }
  • remove方法
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));
            else {
            	// 需要移动的索引值大于0则需要分段拷贝至新数组
                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();
        }
    }

CopyOnWriteArrayList变更数据总结:

  • 不管是新增、修改还是删除一个元素,都需要重建数组并且拷贝原数组的全部数据,因此CopyOnWriteArrayList在做数据变更的时候会有更大的资源开销
  • 在数据变更操作是会加上当前实例的ReentrantLock锁保证线程安全
  • 不会出现ConcurrentModificationException异常的原因是读取线程拿到的数组对象与写入线程拿到的数组对象其实是两个不同的对象,因此不会出现该异常
    这里列举的是几个比较简单的数据变更方法,其他的数据变更方法原理是一样的,有兴趣可以自己翻一下源码了解一下

迭代器方法

  • iterator方法
public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
  • COWIterator类的变更方法
public void remove() {
            throw new UnsupportedOperationException();
        }

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

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

CopyOnWriteArrayList的迭代器方法会构造一个COWIterator对象,在该类的实现中所有变更记录的方法都会抛出UnsupportedOperationException,因此在CopyOnWriteArrayList的迭代器方法中只能读取数据不能对数据进行变更操作

一个实例对比CopyOnWriteArrayList和ArrayList

  • 公共类
private static void printAll(List<String> list) {
        System.out.println(list);
    }

    static class ListRunnable implements Runnable {

        private final List<String> list;

        ListRunnable(List<String> list) {
            this.list = list;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                list.add(Thread.currentThread().getName() + "-" + i);
                printAll(list);
            }
        }
    }
  • 多线程操作ArrayList
@Test
    public void notUseJuc() throws InterruptedException {
        List<String> list = new ArrayList<>();
        final Thread th1 = new Thread(new ListRunnable(list));
        final Thread th2 = new Thread(new ListRunnable(list));

        th1.start();
        th2.start();
        th2.join();
    }

运行结果会偶发异常:

[Thread-0-0, Thread-1-0]
[Thread-0-0, Thread-1-0, Thread-1-1]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2, Thread-1-3]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2, Thread-1-3, Thread-1-4]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2, Thread-1-3, Thread-1-4, Thread-1-5]
Exception in thread "Thread-0" java.util.ConcurrentModificationException
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2, Thread-1-3, Thread-1-4, Thread-1-5, Thread-1-6]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2, Thread-1-3, Thread-1-4, Thread-1-5, Thread-1-6, Thread-1-7]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2, Thread-1-3, Thread-1-4, Thread-1-5, Thread-1-6, Thread-1-7, Thread-1-8]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-1-2, Thread-1-3, Thread-1-4, Thread-1-5, Thread-1-6, Thread-1-7, Thread-1-8, Thread-1-9]
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.bufan.juc.JucListTest.printAll(JucListTest.java:38)
	at com.bufan.juc.JucListTest.access$000(JucListTest.java:13)
	at com.bufan.juc.JucListTest$ListRunnable.run(JucListTest.java:53)
	at java.lang.Thread.run(Thread.java:748)
  • 多线程操作CopyOnWriteArrayList
@Test
    public void useJuc() throws InterruptedException {
        List<String> list = new CopyOnWriteArrayList<>();
        final Thread th1 = new Thread(new ListRunnable(list));
        final Thread th2 = new Thread(new ListRunnable(list));

        th1.start();
        th2.start();
        th2.join();
    }

运行结果正常,不会抛ConcurrentModificationException异常:

[Thread-0-0, Thread-1-0]
[Thread-0-0, Thread-1-0]
[Thread-0-0, Thread-1-0, Thread-1-1]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6, Thread-0-6]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6, Thread-0-6, Thread-1-7]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6, Thread-0-6, Thread-1-7, Thread-0-7]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6, Thread-0-6, Thread-1-7, Thread-0-7, Thread-1-8]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6, Thread-0-6, Thread-1-7, Thread-0-7, Thread-1-8, Thread-0-8]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6, Thread-0-6, Thread-1-7, Thread-0-7, Thread-1-8, Thread-0-8, Thread-1-9]
[Thread-0-0, Thread-1-0, Thread-1-1, Thread-0-1, Thread-1-2, Thread-0-2, Thread-1-3, Thread-0-3, Thread-1-4, Thread-0-4, Thread-1-5, Thread-0-5, Thread-1-6, Thread-0-6, Thread-1-7, Thread-0-7, Thread-1-8, Thread-0-8, Thread-1-9, Thread-0-9]
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值