并发编程6:CopyOnWriteArrayList 的写时复制

来源:互联网 发布:音乐降调软件手机 编辑:程序博客网 时间:2024/06/06 07:21

首先提个问题:

  • 线程安全的 List 集合有什么?
  • CopyOnWriteArrayList 的特点以及使用场景?

如果这个问题你答不上来,那这篇文章可能就对你有些价值。

读完本文你将了解:

  • CopyOnWriteArrayList简介
  • CopyOnWriteArrayList源码分析Android SDK25
    • 底部实现
    • 读取操作
    • 修改操作
    • 迭代器
  • CopyOnWriteArrayList源码JDK8有什么不同
  • 总结
    • 优缺点
  • Thanks

CopyOnWriteArrayList 简介

在 Java 中多线程读写 List,有两种选择:

  1. Vector
  2. CopyOnWriteArrayList

Java 集合深入理解:古老的 Vector 中我们了解到 Vector 几乎所有的读写操作都加了 synchronized ,意味着在多线程环境下,如果有多个线程同时想要操作这个 Vector,必须串行排队等着前面的操作完了才能下一个。

就好比我和小肉一起写一个项目,每次得等她完全写完要写的,我才能接着写,这效率实在差了点。

好在有 Git,小肉想帮我修改程序员审美 UI 时,可以拷贝一份代码进行修改,修改完后再合并到远程。

git 多人协作流程: 
当有人要修改代码时,拉取服务端代码到本地,先修改本地的代码,然后再提交到服务器,这样可以避免直接修改远端代码对别人造成的影响。 
这里写图片描述

CopyOnWriteArrayList 是一个线程安全的随机访问列表,实现了 List 接口:

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable {    //...}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

它的思想和 Git 有些相似,即使在多个线程中被并发访问, CopyOnWriteArrayList 的读操作(比如 get())也不会阻塞其他操作;写操作则是通过复制一份,对复制版本进行操作,不会影响原来的数据。

和 Vector 相对效率提高不少。缺点就是可能读取的不是最新的值。

批量操作(比如 addAll()clear())是原子的,也就是说不存在重排序导致的未赋值就访问等情况。

在 Java 集合源码解析:Iterator 中我们了解到:

在调用迭代器的 next,remove 方法时都会比较 expectedModCount 和 modCount 是否相等,如果不相等就会抛出 ConcurrentModificationException ,也就是成为了 fail-fast。

如果需要在遍历时操作列表,其中一种解决办法就是使用 CopyOnWriteArrayList ,它的迭代器永远不会抛出 ConcurrentModificationException异常。原因在于:在创建一个迭代器时,它会拷贝一份列表数据,这样即使操作列表也不会影响迭代器,缺点和前面一样,可能无法反映数据的最新状态。

CopyOnWriteArrayList 源码分析(Android SDK 25)

从名字就可以看出来 CopyOnWriteArrayList 的特点是 “CopyOnWrite”(写时复制),即在写入新元素时不直接操作原容器,而是先复制一个快照,对这个快照进行操作,在操作结束后再将原容器的引用指向新引用。

底部实现

private transient volatile Object[] elements;
  • 1
  • 1

CopyOnWriteArrayList 底层也是一个数组,不同之处在于它被 volatile 修饰,对它的修改、读取具有“可见性”、“有序性”(但不保证原子性),因此可以不加同步块,直接读取。

有三种构造函数:

public CopyOnWriteArrayList() {    elements = EmptyArray.OBJECT;}@SuppressWarnings("unchecked")public CopyOnWriteArrayList(Collection<? extends E> collection) {    this((E[]) collection.toArray());}public CopyOnWriteArrayList(E[] array) {    this.elements = Arrays.copyOf(array, array.length, Object[].class);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

分别创建了空的数组、使用指定集合和数组来创建新数组。

读取操作

先看简单的几个读取操作:

public int size() {    return elements.length;}@SuppressWarnings("unchecked")public E get(int index) {    return (E) elements[index];}public boolean isEmpty() {    return elements.length == 0;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

就是直接读取数组的内容,简单粗暴,快速高效。

修改操作

读取时轻松,但修改时就需要做同步操作了。先看添加元素 add()

public synchronized boolean add(E e) {    Object[] newElements = new Object[elements.length + 1];    System.arraycopy(elements, 0, newElements, 0, elements.length);    newElements[elements.length] = e;    //这步执行前,其他线程访问的就是旧数据    elements = newElements;    return true;}public synchronized void add(int index, E e) {    Object[] newElements = new Object[elements.length + 1];    System.arraycopy(elements, 0, newElements, 0, index);    newElements[index] = e;    System.arraycopy(elements, index, newElements, index + 1, elements.length - index);    elements = newElements;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到,在添加元素时调用 System.arraycopy() 拷贝了一个新数组,然后添加进去元素,最后将原数组的引用修改为拷贝数组。

如果其他线程在“原始数组引用更新”之前读取数据,那它访问到的就是旧数据。

方法被 synchronized 修饰,因此在这个过程中,其他调用这个方法的线程都会阻塞等待,只有这个方法结束后才有获得锁的机会。

set() 方法也是类似的:

public synchronized E set(int index, E e) {    Object[] newElements = elements.clone();    @SuppressWarnings("unchecked")    E result = (E) newElements[index];    newElements[index] = e;    elements = newElements;    return result;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

删除操作 `remove() 也是类似:

public synchronized E remove(int index) {    @SuppressWarnings("unchecked")    E removed = (E) elements[index];    removeRange(index, index + 1);    return removed;}public synchronized boolean remove(Object o) {    int index = indexOf(o);    if (index == -1) {        return false;    }    remove(index);    return true;}private void removeRange(int from, int to) {    Object[] newElements = new Object[elements.length - (to - from)];    System.arraycopy(elements, 0, newElements, 0, from);    System.arraycopy(elements, to, newElements, from, elements.length - to);    elements = newElements;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

迭代器

public Iterator<E> iterator() {    Object[] snapshot = elements;    //读取的是快照    return new CowIterator<E>(snapshot, 0, snapshot.length);}//读取的也是快照,并且不支持 remove() public ListIterator<E> listIterator(int index) {    Object[] snapshot = elements;    if (index < 0 || index > snapshot.length) {        throw new IndexOutOfBoundsException("index=" + index + ", length=" + snapshot.length);    }    CowIterator<E> result = new CowIterator<E>(snapshot, 0, snapshot.length);    result.index = index;    return result;}/** * Equivalent to {@code listIterator(0)}. */public ListIterator<E> listIterator() {    Object[] snapshot = elements;    return new CowIterator<E>(snapshot, 0, snapshot.length);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

返回的迭代器是 CowIterator,实现了 ListIterator 接口:

   static class CowIterator<E> implements ListIterator<E> {        private final Object[] snapshot;        private final int from;        private final int to;        private int index = 0;        CowIterator(Object[] snapshot, int from, int to) {            this.snapshot = snapshot;            this.from = from;            this.to = to;            this.index = from;        }        public void add(E object) {            throw new UnsupportedOperationException();        }        //...        public void remove() {            throw new UnsupportedOperationException();        }        public void set(E object) {            throw new UnsupportedOperationException();        }        //...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

可以看到 CopyOnWriteArrayList 或者它的子列表返回的迭代器不能修改列表数据,尤其是 Iterator.remove()ListIterator.add()ListIterator.set() 方法都会抛出 UnsupportedOperationException 异常。

CopyOnWriteArrayList 源码 JDK8 有什么不同

看完 Android-25 的 CopyOnWriteArrayList 代码,再去看 JDK8 的,发现两者差别有点大啊,真是坑。

在知乎的一个问题下了解了 android SDK 的 Java 源码为什么和 JDK 不一致,摘抄如下:

Android 使用的Java 库是Apache的Harmony, 与官方Java库接口相同,里面实现不一样。

就在06年的时候Sun公司宣布会将JDK开源,但是直到2009年4月才正式发布,而Android已经在2008年发布第一步部智能手机了。所以Google最早的时候使用了当时Apache的Java开源实现Harmony项目。

可惜Sun公司一直不承认Harmony,前不久Harmony那一帮人怒了,给Oracle放狠话说再不承认我我就抵制Java7,结果反倒把Google吓坏了,于是就出现了google宣布切换到openjdk上这条新闻。在Android N上,已经切换到OpenJDK了;基本可以认为Android SDK下的源码与JDK相同。

作者:weishu 
链接:https://www.zhihu.com/question/40103672/answer/84730730 
来源:知乎

JDK8 中的 CopyOnWriteArrayList 底层实现也是数组,不同之处在于使用 Lock 而不是 synchronized 进行同步:

public class CopyOnWriteArrayList<E>    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {    private static final long serialVersionUID = 8673264195747942595L;    //锁住所有修改操作    final transient ReentrantLock lock = new ReentrantLock();    //只能被getArray/setArray 访问    private transient volatile Object[] array;    //...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

构造函数也有些区别:

final void setArray(Object[] a) {    array = a;}public CopyOnWriteArrayList() {    setArray(new Object[0]);}public CopyOnWriteArrayList(Collection<? extends E> c) {    Object[] elements;    if (c.getClass() == CopyOnWriteArrayList.class)        elements = ((CopyOnWriteArrayList<?>)c).getArray();    else {        elements = c.toArray();        // c.toArray might (incorrectly) not return Object[] (see 6260652)        if (elements.getClass() != Object[].class)            elements = Arrays.copyOf(elements, elements.length, Object[].class);    }    setArray(elements);}public CopyOnWriteArrayList(E[] toCopyIn) {    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在使用集合作为参数构造时,做了一些优化:如果这个集合也是 CopyOnWriteArrayList,就直接指向该集合的数组;否则复制一下。

读取操作没什么变化,都是读的数组,不同之处在于通过 getter 方法包了一层:

public int size() {    return getArray().length;}public boolean isEmpty() {    return size() == 0;}private E get(Object[] a, int index) {    return (E) a[index];}public E get(int index) {    return get(getArray(), index);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

添加操作 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();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

唯一的区别就是使用了 ReentrantLock,它和 synchronized 的区别大致如下:

  • synchronized 无法中断一个正在等候获得锁的线程,也无法通过投票得到锁
  • ReentrantLock 拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性

摘自:http://blog.csdn.net/fw0124/article/details/6672522

删除操作也差不多:

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();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

总结

核心思想就两点:

  1. 底部实现(这里是数组) volatile 修饰,保证一致性
  2. 写时复制,写完更新引用

有了这两点,我们自己写个 CopyOnWriteHashMap 也是分分钟的事,有兴趣可以自己写一下。

优缺点

优点:

  • 可以在多线程环境下操作 List
  • 读的效率很高

缺点:

  • 读的可能不是最新值
  • 每次写需要创建个新数组,占用额外内存

可以看到,应该在并发读远大于并发写的情况下使用这个容器,比如保存缓存数据。

类似的思想还有个 CopyOnWriteArraySet

Thanks

http://ifeve.com/java-copy-on-write/ 
http://blog.csdn.net/fw0124/article/details/6672522 
http://coolshell.cn/articles/11175.html