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日,下午。

原文地址:https://www.cnblogs.com/developer_chan/p/11490517.html