Chapter 12 CopyOnWriteArrayList原理分析

    xiaoxiao2021-12-14  17

    1 概述

    JDK1.5的j.u.c库中提供了Copy-On-Write机制,简称为COW。其实现的基本思想是,开始的时候只有一份内容,当某个线程想要修改这个内容的时候,会将其拷贝出去形成一份新的内容,在这份新的内容上进行修改,这是一种延时懒惰策略。用该机制形成的有两个并发容器,即CopyOnWriteArrayList和CopyOnWriteArraySet。其中,后者是基于前者实现的,当进行数据的添加和删除时是在数组上进行遍历的,这和HashSet的哈希表实现以及TreeSet的二叉树实现是不同的,所以其效率是比较低效的,只是实现了元素的唯一性。

    这里主要讨论一下CopyOnWriteArrayList的实现原理。

    2 CopyOnWriteArrayList性能测试

    为了测试CopyOnWriteArrayList的性能,这里将普通的ArrayList与其一起进行测试。测试的过程是,同时开启10个写线程和10个读线程对同一个list进行操作,具体的测试代码如下:

    import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Created by fubinhe on 16/12/1. */ public class CopyOnWriteArrayListDemo { public static final int NUM = 10; public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < NUM; ++i) { list.add("main_" + i); } list.get(0); ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < NUM; ++i) { exec.execute(new WriteTask(list, i)); exec.execute(new ReadTask(list)); } exec.shutdown(); } } class WriteTask implements Runnable { private List<String> list; private int removeIndex; public WriteTask(List<String> list, int removeIndex) { this.list = list; this.removeIndex = removeIndex; } @Override public void run() { list.remove(removeIndex); list.add("write_" + removeIndex); } } class ReadTask implements Runnable { private List<String> list; public ReadTask(List<String> list) { this.list = list; } @Override public void run() { for (String s : list) { System.out.println(s); } } } 运行上述代码,程序将抛出ConcurrentModificationException异常。这是因为,在读线程试图读取下一个元素的时候,该元素恰好被写线程删除了,迭代器失效。如果将第16行代码替换成下面的代码,则可以顺利的执行并发的读写。

    List<String> list = new CopyOnWriteArrayList<>();

    3 源代码分析

    CopyOnWriteArrayList之所以能进行并发的读写,是因为采用了读写分离的思想,读和写是在不用的容器中进行的。下面分析一些其核心方法的源代码。

    3.1 add方法

    add方法的源代码如下,读起来也比较好懂。在开始写之前加锁,然后将原有的数组复制出来,在新数组的结尾加上一个新元素后,将新数组的引用赋给原有的数组引用,最后释放持有的锁。

    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(); } }

    3.2 remove方法

    和普通ArrayList类似,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) 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(); } }

    和add方法类似,其实现方式也比较简单,以某个索引为界分两次将原数组的值拷贝到新数组中。

    3.3 get方法

    CopyOnWriteArrayList在执行读操作时不进行加锁,所以其实现更简单,直接返回数组中某个索引的值。由于没有加锁,所以如果在读的同时有写线程对数组进行操作,那么有可能读到的值并不是最新的。

    public E get(int index) { return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; }

    4 总结

    COW机制使用读写分离的思想提高并发性能,使得多个读写操作能同时进行。但是很明显,其也有两个缺点,即:

    (1)由于读操作没有加锁,所以如果在读的过程中也有写操作在进行,那么无法保证实时的一致性,只能做到最终的一致性。

    (2)占用内存多。因为在对数组进行修改时,需要将原有的数组拷贝到新的数组中,那么内存中会多维护一份数组。如果修改的操作频繁,会造成比正常更多的FULL GC。

    转载请注明原文地址: https://ju.6miu.com/read-971172.html

    最新回复(0)