CopyOnWriteArrayList 中的隐藏的知识,你Get了吗?

线程安全 List

在 Java 中,线程安全的 List 不止一个,除了今天的主角CopyOnWriteArrayList 以外,还有 Vector 类和 SynchronizedList 类,它们都是线程安全的 List 集合。在介绍 CopyOnWriteArrayList 以前,先简单介绍下另外两个。css

若是你尝试你查看它们的源码,你会发现有点不对头,并发集合不都是在 java.util.concurrent包中嘛,为何Vector 类和 SynchronizedList 类 这两个是在 java.util 包里呢?java

确实是这样的,这两个线程安全的 List 和线程安全的 HashTable 是同样的,都是比较简单粗暴的实现方式,直接方法上增长 synchronized 关键字实现的,并且无论增删改查,通通加上,即便是 get 方法也不例外,没错,就是这么粗暴。数组

Vector 类的 get 方法:安全

// Vector 中的 get 操做添加了 synchronizedpublic synchronized E get(int index) {    if (index >= elementCount)        throw new ArrayIndexOutOfBoundsException(index);    return elementData(index);
}

SynchronizedList 类的 ge t 方法:并发

public E get(int index) {
   synchronized (mutex) {return list.get(index);}
}

同窗不妨思考一下,其实在 get 方法上添加同步机制也是有缘由的,虽然下降了效率,可是可让写入的数据当即能够被查询到,这也保证了数据的强一致性。另外上面关于 synchronized 简单粗暴的描述也是不够准确的,由于在高版本的 JDK 中,synchronized 已经能够根据运行时状况,自动调整锁的粒度,后面介绍 CopyOnWriteArrayList 时会再次讲到。app

CopyOnWriteArrayList

在 JDK 并发包中,目前关于 List 的并发集合,只有 CopyOnWriteArrayList 一个。上面简单介绍了 Vector 和 SynchronizdList 的粗暴实现,既然还有 CopyOnWriteArrayList,那么它必定是和上面两种是有区别的,做为惟一的并发 List,它有什么不一样呢?ide

在探究 CopyOnWriteArrayList 的实现以前,咱们不妨先思考一下,若是是你,你会怎么来实现一个线程安全的 List。函数

  1. 并发读写时该怎么保证线程安全呢?源码分析

  2. 数据要保证强一致性吗?数据读写更新后是否马上体现?性能

  3. 初始化和扩容时容量给多少呢?

  4. 遍历时要不要保证数据的一致性呢?须要引入 Fail-Fast 机制吗?

经过类名咱们大体能够猜想到 CopyOnWriteArrayList 类的实现思路:Copy-On-Write, 也就是写时复制策略;末尾的 ArrayList 表示数据存放在一个数组里。在对元素进行增删改时,先把现有的数据数组拷贝一份,而后增删改都在这个拷贝数组上进行,操做完成后再把原有的数据数组替换成新数组。这样就完成了更新操做。

可是这种写入时复制的方式一定会有一个问题,由于每次更新都是用一个新数组替换掉老的数组,若是不巧在更新时有一个线程正在读取数据,那么读取到的就是老数组中的老数据。其实这也是读写分离的思想,放弃数据的强一致性来换取性能的提高。

分析源码 ( JDK8 )

上面已经说了,CopyOnWriteArrayList 的思想是写时复制,读写分离,它的内部维护着一个使用 volatile 修饰的数组,用来存放元素数据。

/** The array, accessed only via getArray/setArray. */private transient volatile Object[] array;

CopyOnWriteArrayList 类中方法不少,这里不会一一介绍,下面会分析其中的几个经常使用的方法,这几个方法理解后基本就能够掌握 CopyOnWriteArrayList 的实现原理。

构造函数

CopyOnWriteArrayList 的构造函数一共有三个,一个是无参构造,直接初始化数组长度为0;另外两个传入一个集合或者数组做为参数,而后会把集合或者数组中的元素直接提取出来赋值给 CopyOnWriteArrayList 内部维护的数组。

// 直接初始化一个长度为 0 的数组
public CopyOnWriteArrayList() {
   setArray(new Object[0]);
}
// 传入一个集合,提取集合中的元素赋值到 CopyOnWriteArrayList 数组
public CopyOnWriteArrayList(Collection<!--? extends E--> c) {
   Object[] es;
   if (c.getClass() == CopyOnWriteArrayList.class)
       es = ((CopyOnWriteArrayList<!--?-->)c).getArray();
   else {
       es = c.toArray();
       if (c.getClass() != java.util.ArrayList.class)
           es = Arrays.copyOf(es, es.length, Object[].class);
   }
   setArray(es);
}
// 传入一个数组,数组元素提取后赋值到 CopyOnWriteArrayList 数组
public CopyOnWriteArrayList(E[] toCopyIn) {
   setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

构造函数是实例建立时调用的,没有线程安全问题,因此构造方法都是简单的赋值操做,没有特殊的逻辑处理。

新增元素

元素新增根据入参的不一样有好几个,可是原理都是同样的,因此下面只贴出了 add(E e ) 的实现方式,是经过一个 ReentrantLock 锁保证线程安全的。

/**
* 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) {
   final ReentrantLock lock = this.lock;    lock.lock(); // 加锁
   try {
       Object[] elements = getArray(); // 获取数据数组
       int len = elements.length;
       Object[] newElements = Arrays.copyOf(elements, len + 1); // 拷贝一个数据数组,长度+1
       newElements[len] = e; // 加入新元素
       setArray(newElements); // 用新数组替换掉老数组
       return true;
   } finally {        lock.unlock();
   }
}

具体步骤:

  1. 加锁,获取目前的数据数组开始操做(加锁保证了同一时刻只有一个线程进行增长/删除/修改操做)。

  2. 拷贝目前的数据数组,且长度增长一。

  3. 新数组中放入新的元素。

  4. 用新数组替换掉老的数组。

  5. finally 释放锁。

因为每次 add 时容量只增长了1,因此每次增长时都要建立新的数组进行数据复制,操做完成后再替换掉老的数据,这必然会下降数据新增时候的性能。下面经过一个简单的例子测试 CopyOnWriteArrayList 、Vector、ArrayList 的新增和查询性能。

public static void main(String[] args) {
   CopyOnWriteArrayList<object> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
   Vector vector = new Vector<>();
   ArrayList arrayList = new ArrayList();

   add(copyOnWriteArrayList);
   add(vector);
   add(arrayList);

   get(copyOnWriteArrayList);
   get(vector);
   get(arrayList);
}public static void add(List list) {    long start = System.currentTimeMillis();    for (int i = 0; i < 100000; i++) {        list.add(i);
   }    long end = System.currentTimeMillis();
   System.out.println(list.getClass().getName() + ".size=" + list.size() + ",add耗时:" + (end - start) + "ms");
}public static void get(List list) {    long start = System.currentTimeMillis();    for (int i = 0; i < list.size(); i++) {
       Object object = list.get(i);
   }    long end = System.currentTimeMillis();
   System.out.println(list.getClass().getName() + ".size=" + list.size() + ",get耗时:" + (end - start) + "ms");
}

从测得的结果中能够看到 CopyOnWriteArrayList 的新增耗时最久,其次是加锁的 Vector(Vector 的扩容默认是两倍)。而在获取时最快的是线程不安全的 ArrayList,其次是 CopyOnWriteArrayList,而 Vector 由于 Get 时加锁,性能最低。

java.util.concurrent.CopyOnWriteArrayList.size=100000,add耗时:2756msjava.util.Vector.size=100000,add耗时:4msjava.util.ArrayList.size=100000,add耗时:3msjava.util.concurrent.CopyOnWriteArrayList.size=100000,get耗时:4msjava.util.Vector.size=100000,get耗时:5msjava.util.ArrayList.size=100000,get耗时:2ms

修改元素

修改元素和新增元素的思想是一致的,经过 ReentrantLock 锁保证线程安全性,实现代码也比较简单,原本不许备写进来的,可是在看源码时发现一个很是有意思的地方,看下面的代码。

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

经过源码能够看到在修改元素前会先比较修改先后的值是否相等,而在相等的状况下,依旧 setArray(elements); 这就很奇妙了,究竟是为何呢?想了解其中的缘由须要了解下 volatile 的特殊做用,经过下面这个代码例子说明。

// initial conditionsint nonVolatileField = 0;
CopyOnWriteArrayList<string> list = /* a single String */// Thread 1nonVolatileField = 1;                 // (1)list.set(0, "x");                     // (2)// Thread 2String s = list.get(0);               // (3)if (s == "x") {    int localVar = nonVolatileField;  // (4)}// 例子来自:https://stackoverflow.com/questions/28772539/why-setarray-method-call-required-in-copyonwritearraylist

要想理解例子中的特殊之处,首先你要知道 volatile 能够防止指令重排,其次要了解 happens-before 机制。说简单点就是它们能够保证代码的执行先后顺序。

好比上面例子中的代码,1 会在 2 以前执行,3 会在 4 以前执行,这都没有疑问。还有一条是 volatile 修饰的属性写会在读以前执行,因此 2会在 3 以前执行。而执行顺序还存在传递性。因此最终 1 会在 4 以前执行。这样 4 获取到的值就是步骤 1 为 nonVolatileField 赋的值。若是 CopyOnWriteArrayList 中的 set 方法内没有为相同的值进行 setArray,那么上面说的这些就都不存在了。

删除元素

remove 删除元素方法一共有三个,这里只看public E 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) // 是否末尾
           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(); // 解锁
   }
}

代码仍是很简单的,使用 ReentrantLock 独占锁保证操做的线程安全性,而后使用删除元素后的剩余数组元素拷贝到新数组,使用新数组替换老数组完成元素删除,最后释放锁返回。

获取元素

获取下标为 index 的元素,若是元素不存在,会抛出IndexOutOfBoundsException 异常。

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

首先看到这里是没有任何的加锁操做的,而获取指定位置的元素又分为了两个步骤:

  1. getArray() 获取数据数组。

  2. get(Object[] a, int index) 返回指定位置的元素。

颇有可能在第一步执行完成以后,步骤二执行以前,有线程对数组进行了更新操做。经过上面的分析咱们知道更新会生成一个新的数组,而咱们第一步已经获取了老数组,因此咱们在进行 get 时依旧在老数组上进行,也就是说另外一个线程的更新结果没有对咱们的本次 get 生效。这也是上面提到的弱一致性问题。

迭代器的弱一致性

List<string> list = new CopyOnWriteArrayList<>();list.add("www.wdbyte.com");list.add("未读代码");

Iterator<string> iterator = list.iterator();list.add("java");while (iterator.hasNext()) {
   String next = iterator.next();
   System.out.println(next);
}

如今 List 中添加了元素 www.wdbyte.com 和 未读代码,在拿到迭代器对象后,又添加了新元素 java ,能够看到遍历的结果没有报错也没有输出 java 。也就是说拿到迭代器对象后,元素的更新不可见。

www.wdbyte.com未读代码

这是为何呢?要先从CopyOnWriteArrayList 的 iterator() 方法的实现看起。

public Iterator<e> iterator() {    return new COWIterator<e>(getArray(), 0);
}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;
   }
......

能够看到在获取迭代器时,先 getArray() 拿到了数据数组 而后传入到 COWIterator 构造器中,接着赋值给了COWIterator 中的 snapshot 属性,结合上面的分析结果,能够知道每次更新都会产生新的数组,而这里使用的依旧是老数组,因此更新操做不可见,也就是上面屡次提到的弱一致性。

新版变化

上面的源码分析都是基于 JDK 8 进行的。写文章时顺便看了下新版的实现方式有没有变化,还真的有挺大的改变,主要体如今加锁的方式上,或许是由于 JVM 后来引入了 synchronized 锁升级策略,让 synchronized 性能有了很多提高,因此用了 synchronized 锁替换了老的 ReentrantLock 锁。

新增:

public boolean add(E e) {    synchronized (lock) {
       Object[] es = getArray();        int len = es.length;
       es = Arrays.copyOf(es, len + 1);
       es[len] = e;
       setArray(es);        return true;
   }
}

修改:

public E set(int index, E element) {
   synchronized (lock) {
       Object[] es = getArray();
       E oldValue = elementAt(es, index);        if (oldValue != element) {
           es = es.clone();
           es[index] = element;
       }
       // Ensure volatile write semantics even when oldvalue == element
       setArray(es);        return oldValue;
   }
}

总结

经过上面的分析,获得下面几点关于 CopyOnWriteArrayList 的总结。

  1. CopyOnWriteArrayList 采用读写分离,写时复制方式实现线程安全,具备弱一致性。

  2. CopyOnWriteArrayList 由于每次写入时都要扩容复制数组,写入性能不佳。

  3. CopyOnWriteArrayList 在修改元素时,为了保证 volatile 语义,即便元素没有任何变化也会从新赋值,

  4. 在高版 JDK 中,得益于 synchronized 锁升级策略, CopyOnWriteArrayList 的加锁方式采用了 synchronized。

相关文章
相关标签/搜索