一、demo
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) throws Exception{
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
list.add("a1");
System.out.println(list);
}
}
二、构造方法源码分析
从下面这段构造函数的代码,就可以看出来,CopyOnWriteArrayList其实也是底层基于数组来实现的,volatile修饰,保证多线程读写的可见性,只要有一个线程修改了这个数组,其他的线程立马是可以读到的;
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
private transient volatile Object[] array;
final void setArray(Object[] a) {
array = a;
}
三、add方法源码分析
final transient ReentrantLock lock = new ReentrantLock();
public boolean add(E e) {
/** 通过ReentrantLock进行加锁,保证同一时间只能有一个线程来操作底层的数组数据结构
* 其实就可以判断更新CopyOnWriteArrayList的时候
* 肯定都会通过ReentrantLock进行加锁来保证多线程并发安全的
* 加锁原理不清楚的可以回过头看看我之前分析的ReentrantLock源码分析
*/
final ReentrantLock lock = this.lock;
lock.lock();
try {
/** 获取当前数组及数组长度 */
Object[] elements = getArray();
int len = elements.length;
/** 将当前数组元素复制到新数组里面去,新数组长度为当前数组长度加一 */
Object[] newElements = Arrays.copyOf(elements, len + 1);
/** 将添加的元素更新至新数组的最后一位 */
newElements[len] = e;
/** 将新数组设置到CopyOnWriteArrayList里持有的数组里面去 */
setArray(newElements);
return true;
} finally {
/** 锁释放 */
lock.unlock();
}
}
四、set方法源码分析
public E set(int index, E element) {
/** 获取ReentrantLock锁进行加锁 */
final ReentrantLock lock = this.lock;
lock.lock();
try {
/** 获取数组,并获取指定下标中的数组元素 */
Object[] elements = getArray();
E oldValue = get(elements, index);
/** 判断是否一致,不一致进行修改,一致将数组更新后返回 */
if (oldValue != element) {
/** 获取数组长度 */
int len = elements.length;
/** 将老数组的元素赋值到新数组里面,长度和老数组一样 */
Object[] newElements = Arrays.copyOf(elements, len);
/** 更新新数组指定下标元素 */
newElements[index] = element;
/** 更新持有数组 */
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
/** 释放锁 */
lock.unlock();
}
}
五、remove方法源码分析
public E remove(int index) {
/** 获取ReentrantLock锁进行加锁 */
final ReentrantLock lock = this.lock;
lock.lock();
try {
/** 获取数组,并获取指定下标中的数组元素 */
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
/** 判断第一段拷贝数组数量 如:len = 5,index=2 numMoved=5-2-1=2 */
int numMoved = len - index - 1;
/** 判断删除元素是否为最后一位
* 如果是则将老数组元素赋值给新数组,忽略最后一位元素不进行赋值
* 新数组大小为:老数组大小-1
*/
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
/** 构建新数组,因为是删除元素,所以新数组大小为老数组长度-1 */
Object[] newElements = new Object[len - 1];
/** 把老数组里的,从index = 0开始的元素,截止到index = 2的元素
* 复制到新数组的从index = 0位置开始的地方
* 复制的数据为index = 0、index = 1
*/
System.arraycopy(elements, 0, newElements, 0, index);
/** index = 2,从老数组的index = 3,拷贝2个元素
* index = 3和index = 4的两个元素给拷贝到了新数组里去
* 从新数组的index = 2的位置开始放置
*/
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
/** 将新数组赋值给CopyOnWriteArrayList中的数组变量 */
setArray(newElements);
}
return oldValue;
} finally {
/** 释放锁 */
lock.unlock();
}
}
六、get方法源码分析
直接从底层数组里来读取数据,通过index定位对应位置的元素,这个是不加锁的;
因为写数据的时候,会复制一个副本,新的数组,对新的数组来修改,修改好了设置回去就可以了。所以在写数据的同时读数据其实不是读的同一份数据,所以读和写之间是没有锁的冲突的,读只有两种情况:
第一种:我读到的老数组的数据;
第二种:其他线程更新好了数组,volatile写,我读到的是新数组的数据;
public E get(int index) {
/** 获取数组,在根据index获取数组中的元素 */
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
七、iterator方法源码分析
iterator其实是拷贝了一个数组副本,基于这个副本进行迭代的,如果在这个时期CopyOnWriteArrayList添加了元素迭代线程是感知不到的,会继续将之前数组里面元素迭代出来;
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
/** 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 E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
总结
缺点
弱一致性或者最终一致性:多个线程并发的读写list,写的时候是需要一段时间,写好之前此时其他线程读到的都是老数组的数据,这个过程中,多个线程看到的数据是不一致的,人家修改了数据没有立马被人读到;
空间换时间:写的时候,经常内存里会出现复制出来的一模一样的副本,对内存消耗过大;
优点
读和写不互斥的,写和写互斥,同一时间就一个人可以写,但是写的同时可以允许其他所有人来读;读和读也是并发的;读写锁机制还要好;