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