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