• CopyOnWriteArrayList源码分析


    前言:CopyOnWriteArrayList为ArrayList的线程安全版本,这里来分析下其内部是如何实现的。

    注:本文jdk源码版本为jdk1.8.0_172


    1.CopyOnWriteArrayList介绍

    CopyOnWriteArrayList是ArrayList的线程安全版本,因此其底层数据结构也是数组,但是在写操作的时候都会拷贝一份数据进行修改,修改完后替换掉老数据,从而保证只阻塞写操作,读操作不会阻塞,实现读写分离。

    1 public class CopyOnWriteArrayList<E>
    2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

    2.具体源码分析

    底层数据结构:

    1     /** The lock protecting all mutators */
    2     // 使用可重入锁进行加锁,保证线程安全
    3     final transient ReentrantLock lock = new ReentrantLock();
    4 
    5     /** The array, accessed only via getArray/setArray. */
    6     // 底层数据结构,注意这里用volatile修饰,确定了多线程情况下的可见性
    7     private transient volatile Object[] array;

    分析:

    注意array数组只能通过getArray和setArray函数进访问。

    构造函数:

     1 public CopyOnWriteArrayList() {
     2     // 所有对array的操作都是通过setArray和getArray进行的
     3     setArray(new Object[0]);
     4 }
     5 
     6  public CopyOnWriteArrayList(Collection<? extends E> c) {
     7     Object[] elements;
     8     // 如果c是CopyOnWriteArrayList则把数组直接进行赋值,注意这里是浅拷贝,两个集合公用一个数组
     9     if (c.getClass() == CopyOnWriteArrayList.class)
    10         elements = ((CopyOnWriteArrayList<?>)c).getArray();
    11     else {
    12         elements = c.toArray();
    13         // c.toArray might (incorrectly) not return Object[] (see 6260652)
    14         if (elements.getClass() != Object[].class)
    15             elements = Arrays.copyOf(elements, elements.length, Object[].class);
    16     }
    17     setArray(elements);
    18 }

    分析:

    从构造函数中可以了解两点:

    #1.CopyOnWriteArrayList默认容量是数组长度为1的Object类型数组

    #2.操作array底层数组,都是通过setArray和getArray来进行的。

    add(e):

     1 public boolean add(E e) {
     2     final ReentrantLock lock = this.lock;
     3     lock.lock();
     4     try {
     5         Object[] elements = getArray();
     6         int len = elements.length;
     7         // 注意这里将数组长度加1
     8         Object[] newElements = Arrays.copyOf(elements, len + 1);
     9         // 新元素放在最后一位
    10         newElements[len] = e;
    11         setArray(newElements);
    12         return true;
    13     } finally {
    14         lock.unlock();
    15     }
    16 }

    分析:

    add操作是加了锁的,利用了ReentrantLock进行加锁,注意使用该方式进行加锁,需要手动释放。

    整个过程是新建了一个新的数组(数组长度加1),然后将新元素放在最后一位,最后替换掉旧数组。

    add(index,e):

     1 public void add(int index, E element) {
     2     final ReentrantLock lock = this.lock;
     3     lock.lock();
     4     try {
     5         Object[] elements = getArray();
     6         int len = elements.length;
     7         // 越界判断
     8         if (index > len || index < 0)
     9             throw new IndexOutOfBoundsException("Index: "+index+
    10                                                 ", Size: "+len);
    11         Object[] newElements;
    12         int numMoved = len - index;
    13         if (numMoved == 0)
    14             // 插入位置在最后一位,拷贝一个n+1的数组,前n个元素与旧数组一致
    15             newElements = Arrays.copyOf(elements, len + 1);
    16         else {
    17             // 插入位置不是最后一位
    18             // 先新建一个n+1的数组
    19             newElements = new Object[len + 1];
    20             // 拷贝旧数组前index的元素到新数组中
    21             System.arraycopy(elements, 0, newElements, 0, index);
    22             // 将index之后的元素往后挪一位到新数组中,这样正好index位置是空出来的
    23             System.arraycopy(elements, index, newElements, index + 1,
    24                              numMoved);
    25         }
    26         // 将元素放在index处
    27         newElements[index] = element;
    28         setArray(newElements);
    29     } finally {
    30         lock.unlock();
    31     }
    32 }

    分析:

    在指定位置上插入元素的逻辑其实也不复杂(同样进行了加锁)。

    #1.首先判断了index是否越界。

    #2.根据插入位置进行操作,是否在最后一位。

    get操作:

    1 public E get(int index) {
    2     // 读取元素不需要加锁
    3     // 这里并未做数组越界检查,因为数组本身会做越界检查
    4     return get(getArray(), index);
    5 }
    6 
    7    private E get(Object[] a, int index) {
    8     return (E) a[index];
    9 }

    分析:

    get操作其实非常简单,直接从数组中获取元素即可,注意此时并未加锁,并且未做数组越界检查。

    remove操作:

     1 public E remove(int index) {
     2     final ReentrantLock lock = this.lock;
     3     lock.lock();
     4     try {
     5         Object[] elements = getArray();
     6         int len = elements.length;
     7         E oldValue = get(elements, index);
     8         int numMoved = len - index - 1;
     9         // 元素在最后一位
    10         if (numMoved == 0)
    11             setArray(Arrays.copyOf(elements, len - 1));
    12         else {
    13             // 新建一个n-1数组
    14             Object[] newElements = new Object[len - 1];
    15             // 拷贝前index的元素到新数组
    16             System.arraycopy(elements, 0, newElements, 0, index);
    17             // index之后的元素往前移动一位,就把index删除了
    18             System.arraycopy(elements, index + 1, newElements, index,
    19                              numMoved);
    20             setArray(newElements);
    21         }
    22         return oldValue;
    23     } finally {
    24         lock.unlock();
    25     }
    26 }

    分析:

    注意该操作加锁了,整个逻辑比较简单,通过以上注释理解应该不困难,这里就不再赘述了。

    retainAll:求交集,在ArrayList中也有求交集的函数,这里来看看CopyOnWriteArrayList是如何求交集的。

     1 public boolean retainAll(Collection<?> c) {
     2         // 判空
     3         if (c == null) throw new NullPointerException();
     4         final ReentrantLock lock = this.lock;
     5         // 加锁
     6         lock.lock();
     7         try {
     8             // 取出数组
     9             Object[] elements = getArray();
    10             int len = elements.length;
    11             if (len != 0) {
    12                 // temp array holds those elements we know we want to keep
    13                 int newlen = 0;
    14                 Object[] temp = new Object[len];
    15                 // 遍历数组
    16                 for (int i = 0; i < len; ++i) {
    17                     Object element = elements[i];
    18                     // 在c集合中包含该元素,则进行插入
    19                     if (c.contains(element))
    20                         temp[newlen++] = element;
    21                 }
    22                 // 交集数组长度与原数组长度不一致
    23                 if (newlen != len) {
    24                     // 设置新的数组
    25                     setArray(Arrays.copyOf(temp, newlen));
    26                     return true;
    27                 }
    28             }
    29             return false;
    30         } finally {
    31             lock.unlock();
    32         }
    33     }

    分析:

    求交集的操作与ArrayList大致相同,这里不再进行赘述。

    removeAll:求差集,注意这里求的是单向差集,只保留当前集合不在C集合中的元素,与ArrayList一致。

     1 public boolean removeAll(Collection<?> c) {
     2     // 判空处理
     3     if (c == null) throw new NullPointerException();
     4     final ReentrantLock lock = this.lock;
     5     // 加锁
     6     lock.lock();
     7     try {
     8         Object[] elements = getArray();
     9         int len = elements.length;
    10         if (len != 0) {
    11             // temp array holds those elements we know we want to keep
    12             int newlen = 0;
    13             Object[] temp = new Object[len];
    14             // 遍历数组
    15             for (int i = 0; i < len; ++i) {
    16                 Object element = elements[i];
    17                 // 如果元素不包含在C集合中,则进行处理
    18                 if (!c.contains(element))
    19                     temp[newlen++] = element;
    20             }
    21             // 差集长度与原数组长度不一致
    22             if (newlen != len) {
    23                 setArray(Arrays.copyOf(temp, newlen));
    24                 return true;
    25             }
    26         }
    27         return false;
    28     } finally {
    29         lock.unlock();
    30     }
    31 }

    分析:

    求差集操作与上面retainAll的操作正好相反,这里不做过多赘述。

    这里只分析了笔者认为相对重要的源码,其实CopyOnWriteArrayList中的源码还比较多,可自行进行分析,其实逻辑都不是很复杂。

    3.总结

    #1.CopyOnWriteArrayList线程安全,默认容量为长度为1的Object数组,允许元素为null。

    #2.使用ReentrantLock可重入锁,保证线程安全。

    #3.在写操作时,都需要拷贝一份数组,然后在拷贝的数组中进行相应的操作,最后再替换旧数组。

    #4.采用读写分离的实现,写操作加锁,读操作不加锁,而且写操作会占用较多空间,因此适用于读多写少的场景。

    #5.CopyOnWriteArrayList能保证最终一致性,但是不保证实时一致性,因为在写操作未完,而进行读操作时,由于写操作在新数组中操作,并不会影响到读操作,这是造成数据不一致性。


    by Shawn Chen,2019.09.14日,下午。

  • 相关阅读:
    2012个人总结与展望
    C++Event机制的简单实现
    string与char*比较 ——why use string
    创建型模式学习总结——设计模式学习笔记
    Bridge模式——设计模式学习笔记
    Singleton模式——设计模式学习
    Adapter模式——设计模式学习笔记
    C++ 申请连续的对象内存
    7 个致命的 Linux 命令与个人评论
    关于保存Activity的状态
  • 原文地址:https://www.cnblogs.com/developer_chan/p/11490517.html
Copyright © 2020-2023  润新知