【JUC】JDK1.8源码分析之CopyOnWriteArrayList(六)

简介:  由于Deque与Queue有很大的相似性,Deque为双端队列,队列头部和尾部都可以进行入队列和出队列的操作,所以不再介绍Deque,感兴趣的读者可以自行阅读源码,相信偶了Queue源码的分析经验,Deque的分析也会水到渠成,下面介绍List在JUC下的CopyOnWriteArrayList类,CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的

一、前言


  由于Deque与Queue有很大的相似性,Deque为双端队列,队列头部和尾部都可以进行入队列和出队列的操作,所以不再介绍Deque,感兴趣的读者可以自行阅读源码,相信偶了Queue源码的分析经验,Deque的分析也会水到渠成,下面介绍List在JUC下的CopyOnWriteArrayList类,CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。


二、CopyOnWriteArrayList数据结构


  通过源码分析可知,CopyOnWriteArrayList使用的数据结构是数组。结构如下

download.png

  说明:CopyOnWriteArrayList底层使用数组来存放元素。


三、CopyOnWriteArrayList源码分析


  3.1 类的继承关系 

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

说明:CopyOnWriteArrayList实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。

 

 3.2 类的内部类


  1. COWIterator类 

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;
        }
        // 是否还有下一项
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        // 是否有上一项
        public boolean hasPrevious() {
            return cursor > 0;
        }
        // next项
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext()) // 不存在下一项,抛出异常
                throw new NoSuchElementException();
            // 返回下一项
            return (E) snapshot[cursor++];
        }
        @SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }
        // 下一项索引
        public int nextIndex() {
            return cursor;
        }
        // 上一项索引
        public int previousIndex() {
            return cursor-1;
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code remove}
         *         is not supported by this iterator.
         */
        // 不支持remove操作
        public void remove() {
            throw new UnsupportedOperationException();
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code set}
         *         is not supported by this iterator.
         */
        // 不支持set操作
        public void set(E e) {
            throw new UnsupportedOperationException();
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code add}
         *         is not supported by this iterator.
         */
        // 不支持add操作
        public void add(E e) {
            throw new UnsupportedOperationException();
        }
        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            Object[] elements = snapshot;
            final int size = elements.length;
            for (int i = cursor; i < size; i++) {
                @SuppressWarnings("unchecked") E e = (E) elements[i];
                action.accept(e);
            }
            cursor = size;
        }
    }

 说明:COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。


  3.3 类的属性  

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 版本序列号
    private static final long serialVersionUID = 8673264195747942595L;
    // 可重入锁
    final transient ReentrantLock lock = new ReentrantLock();
    // 对象数组,用于存放元素
    private transient volatile Object[] array;
    // 反射机制
    private static final sun.misc.Unsafe UNSAFE;
    // lock域的内存偏移量
    private static final long lockOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = CopyOnWriteArrayList.class;
            lockOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("lock"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

 说明:属性中有一个可重入锁,用来保证线程安全访问,还有一个Object类型的数组,用来存放具体的元素。当然,也使用到了反射机制和CAS来保证原子性的修改lock域。


  3.4 类的构造函数


  1. CopyOnWriteArrayList()型构造函数  

 public CopyOnWriteArrayList() {
        // 设置数组
        setArray(new Object[0]);
    }

说明:该构造函数用于创建一个空列表。


  2. CopyOnWriteArrayList(Collection<? extends E>)型构造函数 

  public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class) // 类型相同
            // 获取c集合的数组
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else { // 类型不相同
            // 将c集合转化为数组并赋值给elements
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class) // elements类型不为Object[]类型
                // 将elements数组转化为Object[]类型的数组
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        // 设置数组
        setArray(elements);
    }

  说明:该构造函数用于创建一个按 collection 的迭代器返回元素的顺序包含指定 collection 元素的列表。该构造函数的处理流程如下


  ① 判断传入的集合c的类型是否为CopyOnWriteArrayList类型,若是,则获取该集合类型的底层数组(Object[]),并且设置当前CopyOnWriteArrayList的数组(Object[]数组),进入步骤③;否则,进入步骤②


  ② 将传入的集合转化为数组elements,判断elements的类型是否为Object[]类型(toArray方法可能不会返回Object类型的数组),若不是,则将elements转化为Object类型的数组。进入步骤③


  ③ 设置当前CopyOnWriteArrayList的Object[]为elements。


  3. CopyOnWriteArrayList(E[])型构造函数  

 public CopyOnWriteArrayList(E[] toCopyIn) {
        // 将toCopyIn转化为Object[]类型数组,然后设置当前数组
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

  说明:该构造函数用于创建一个保存给定数组的副本的列表。


  3.5 核心函数分析


  对于CopyOnWriteArrayList的函数分析,主要明白Arrays.copyOf方法即可理解CopyOnWriteArrayList其他函数的意义。


  1. copyOf函数

   public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        // 确定copy的类型(将newType转化为Object类型,将Object[].class转化为Object类型,判断两者是否相等,若相等,则生成指定长度的Object数组
        // 否则,生成指定长度的新类型的数组)
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        // 将original数组从下标0开始,复制长度为(original.length和newLength的较小者),复制到copy数组中(也从下标0开始)
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

  

  说明:该函数用于复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。


  2. 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);
            // 存放元素e
            newElements[len] = e;
            // 设置数组
            setArray(newElements);
            return true;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

 说明:此函数用于将指定元素添加到此列表的尾部,处理流程如下


  ① 获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。


  ② 根据Object数组复制一个长度为length+1的Object数组为newElements(此时,newElements[length]为null),进入步骤③。


  ③ 将下标为length的数组元素newElements[length]设置为元素e,再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。


  3. addIfAbsent 

 private boolean addIfAbsent(E e, Object[] snapshot) {
        // 重入锁
        final ReentrantLock lock = this.lock;
        // 获取锁
        lock.lock();
        try {
            // 获取数组
            Object[] current = getArray();
            // 数组长度
            int len = current.length;
            if (snapshot != current) { // 快照不等于当前数组,对数组进行了修改
                // Optimize for lost race to another addXXX operation
                // 取较小者
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++) // 遍历
                    if (current[i] != snapshot[i] && eq(e, current[i])) // 当前数组的元素与快照的元素不相等并且e与当前元素相等
                        // 表示在snapshot与current之间修改了数组,并且设置了数组某一元素为e,已经存在
                        // 返回
                        return false;
                if (indexOf(e, current, common, len) >= 0) // 在当前数组中找到e元素
                        // 返回
                        return false;
            }
            // 复制数组
            Object[] newElements = Arrays.copyOf(current, len + 1);
            // 对数组len索引的元素赋值为e
            newElements[len] = e;
            // 设置数组
            setArray(newElements);
            return true;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

 说明:该函数用于添加元素(如果数组中不存在,则添加;否则,不添加,直接返回)。可以保证多线程环境下不会重复添加元素,该函数的流程如下

 

 ① 获取锁,获取当前数组为current,current长度为len,判断数组之前的快照snapshot是否等于当前数组current,若不相等,则进入步骤②;否则,进入步骤④

 

 ② 不相等,表示在snapshot与current之间,对数组进行了修改(如进行了add、set、remove等操作),获取长度(snapshot与current之间的较小者),对current进行遍历操作,若遍历过程发现snapshot与current的元素不相等并且current的元素与指定元素相等(可能进行了set操作),进入步骤⑤,否则,进入步骤③


  ③ 在当前数组中索引指定元素,若能够找到,进入步骤⑤,否则,进入步骤④

 

 ④ 复制当前数组current为newElements,长度为len+1,此时newElements[len]为null。再设置newElements[len]为指定元素e,再设置数组,进入步骤⑤


  ⑤ 释放锁,返回。


  4. set函数 

   public E set(int index, E element) {
        // 可重入锁
        final ReentrantLock lock = this.lock;
        // 获取锁
        lock.lock();
        try {
            // 获取数组
            Object[] elements = getArray();
            // 获取index索引的元素
            E oldValue = get(elements, index);
            if (oldValue != element) { // 旧值等于element
                // 数组长度
                int len = elements.length;
                // 复制数组
                Object[] newElements = Arrays.copyOf(elements, len);
                // 重新赋值index索引的值
                newElements[index] = element;
                // 设置数组
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                // 设置数组
                setArray(elements);
            }
            // 返回旧值
            return oldValue;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

 说明:此函数用于用指定的元素替代此列表指定位置上的元素,也是基于数组的复制来实现的。


  5. 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];
                // 复制index索引之前的元素
                System.arraycopy(elements, 0, newElements, 0, index);
                // 复制index索引之后的元素
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                // 设置索引
                setArray(newElements);
            }
            // 返回旧值
            return oldValue;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

 说明:此函数用于移除此列表指定位置上的元素。处理流程如下


  ① 获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],计算需要移动的元素个数(length - index - 1),若个数为0,则表示移除的是数组的最后一个元素,复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②


  ② 先复制index索引前的元素,再复制index索引后的元素,然后设置数组。

 

 ③ 释放锁,返回旧值。


四、示例


  下面通过一个示例来了解CopyOnWriteArrayList的使用

package com.hust.grid.leesf.collections;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
class PutThread extends Thread {
    private CopyOnWriteArrayList<Integer> cowal;
    public PutThread(CopyOnWriteArrayList<Integer> cowal) {
        this.cowal = cowal;
    }
    public void run() {
        try {
            for (int i = 100; i < 110; i++) {
                cowal.add(i);
                Thread.sleep(50);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> cowal = new CopyOnWriteArrayList<Integer>();
        for (int i = 0; i < 10; i++) {
            cowal.add(i);
        }
        PutThread p1 = new PutThread(cowal);
        p1.start();
        Iterator<Integer> iterator = cowal.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        iterator = cowal.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
    }
}

 运行结果(某一次)

0 1 2 3 4 5 6 7 8 9 100 
0 1 2 3 4 5 6 7 8 9 100 101 102 103 

说明:在程序中,有一个PutThread线程会每隔50ms就向CopyOnWriteArrayList中添加一个元素,并且两次使用了迭代器,迭代器输出的内容都是生成迭代器时,CopyOnWriteArrayList的Object数组的快照的内容,在迭代的过程中,往CopyOnWriteArrayList中添加元素也不会抛出异常。


五、总结


  CopyOnWriteArrayList的源码很简单,其主要用到的快照的思路,使得在迭代的过程中,只是Object数组之前的某个快照,而不是最新的Object,这样可以保证在迭代的过程中不会抛出ConcurrentModificationException异常。谢谢各位园友的观看~


目录
相关文章
|
Java
【JUC】JDK1.8源码分析之ThreadPoolExecutor(一)
JUC这部分还有线程池这一块没有分析,需要抓紧时间分析,下面开始ThreadPoolExecutor,其是线程池的基础,分析完了这个类会简化之后的分析,线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。下面开始分析。
84 0
【JUC】JDK1.8源码分析之ThreadPoolExecutor(一)
|
缓存 Java
【JUC】JDK1.8源码分析之SynchronousQueue(九)
本篇是在分析Executors源码时,发现JUC集合框架中的一个重要类没有分析,SynchronousQueue,该类在线程池中的作用是非常明显的,所以很有必要单独拿出来分析一番,这对于之后理解线程池有很有好处,SynchronousQueue是一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。
100 0
【JUC】JDK1.8源码分析之SynchronousQueue(九)
【JUC】JDK1.8源码分析之ConcurrentSkipListSet(八)
分析完了CopyOnWriteArraySet后,继续分析Set集合在JUC框架下的另一个集合,ConcurrentSkipListSet,ConcurrentSkipListSet一个基于 ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
166 0
【JUC】JDK1.8源码分析之ConcurrentSkipListSet(八)
【JUC】JDK1.8源码分析之CopyOnWriteArraySet(七)
分析完了CopyOnWriteArrayList后,下面接着分析CopyOnWriteArraySet,CopyOnWriteArraySet与CopyOnWriteArrayList有莫大的联系,因为CopyOnWriteArraySet的底层是由CopyOnWriteArrayList提供支持,并且将对其的操作转发至对CopyOnWriteArrayList的操作。但是,CopyOnWriteArraySet的元素不允许重复,这是和CopyOnWriteArrayList不相同的地方,下面开始分析。
148 0
【JUC】JDK1.8源码分析之CopyOnWriteArraySet(七)
|
安全
【JUC】JDK1.8源码分析之ConcurrentLinkedQueue(五)
  接着前面的分析,接下来分析ConcurrentLinkedQueue,ConcurerntLinkedQueue一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue是一个恰当的选择。此队列不允许使用null元素。
95 0
【JUC】JDK1.8源码分析之ConcurrentLinkedQueue(五)
|
Java
【JUC】JDK1.8源码分析之LinkedBlockingQueue(四)
 分析完了ArrayBlockingQueue后,接着分析LinkedBlockingQueue,与ArrayBlockingQueue不相同,LinkedBlockingQueue底层采用的是链表结构,其源码也相对比较简单,下面进行正式的分析。
64 0
【JUC】JDK1.8源码分析之LinkedBlockingQueue(四)
|
索引
【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)
在完成Map下的并发集合后,现在来分析ArrayBlockingQueue,ArrayBlockingQueue可以用作一个阻塞型队列,支持多任务并发操作,有了之前看源码的积累,再看ArrayBlockingQueue源码会很容易,下面开始正文。
67 0
【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)
|
Java
【JUC】JDK1.8源码分析之ConcurrentSkipListMap(二)
 最近在做项目的同时也在修复之前项目的一些Bug,所以忙得没有时间看源代码,今天都完成得差不多了,所以又开始源码分析之路,也着笔记录下ConcurrentSkipListMap的源码的分析过程。
95 0
【JUC】JDK1.8源码分析之ConcurrentSkipListMap(二)
|
存储 安全 Java
【JUC】JDK1.8源码分析之ConcurrentHashMap(一)
 最近几天忙着做点别的东西,今天终于有时间分析源码了,看源码感觉很爽,并且发现ConcurrentHashMap在JDK1.8版本与之前的版本在并发控制上存在很大的差别,很有必要进行认真的分析,下面进行源码分析。
105 0
【JUC】JDK1.8源码分析之ConcurrentHashMap(一)
|
缓存
【JUC】JDK1.8源码分析之ReentrantReadWriteLock(七)
在分析了锁框架的其他类之后,下面进入锁框架中最后一个类ReentrantReadWriteLock的分析,它表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁ReadLock和写锁WriteLock,可以通过这两种锁实现线程间的同步,下面开始进行分析。
97 0
【JUC】JDK1.8源码分析之ReentrantReadWriteLock(七)