Skip to main content

CopyOnWriteArrayList源码解析

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

1. CopyOnWrite容器

基本思路就是在多个线程共享同一个内容,当某个线程想修改这个内容的时候,才会真正的把内容拷贝出去形成一个新的内容然后再修改,这是一种延迟懒惰策略

2. 概念

通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。 所以CopyOnWrite容器也是一种读写分离的思想,读和写在不同的容器

2.1 CopyOnWriteArrayList

    /** 重入锁--同步所有的突变操作 */
final transient ReentrantLock lock = new ReentrantLock();

/** 数据保存结构 */
private transient volatile Object[] array;

/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}

/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}

/**
* 初始化长度为0
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}

/**
* 参数为一个集合
*
*
*
* @param c the collection of initially held elements
* @throws NullPointerException if the specified collection is null
*/
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}

/**
* 参数为一个数组
*
* @param toCopyIn the array (a copy of this array is used as the
* internal array)
* @throws NullPointerException if the specified array is null
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

注意:由于当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。所以在往CopyOnWriteArrayList添加数据的时候不要一个个添加,最好的方式是通过带参数的构造方法或者调用addAll的方式添加数据,减少底层的每次拷贝而占用内存和频繁的导致GC操作

2.2 Add方法

public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取原有的数据数组
Object[] elements = getArray();
//数组的长度
int len = elements.length;
//拷贝---长度为len + 1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将新添加的数据设置到最后
newElements[len] = e;
//将array指向新的newElements --内存可见(array变量被volatile修饰)
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}

public void add(int index, E element) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
//释放锁
lock.unlock();
}
}

注意:

  • 采用ReentrantLock保证了同一时刻只有一个写的线程在复制。
  • 数组变量array的应用是用volatile修饰过的,应酬将旧的数组应用指向新的数组;根据volatilehappens-before规则,线程对数组引用的修改对线程是可见的。
  • 由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。

CopyOnWriteArrayList读写示图

3. 总结

优点:

  • 读写是分离的
  • 读线程之间是互不阻塞的
  • 牺牲数据的实时性而保证了最终数据的一致性。即读线程对数据的更新是延迟感知的,因此这种情况下读线程不存在等待的情况。

缺点:

  • 内存占用的问题:因为CopyOnWrite是写的时候复制机制,所以在写操作的时候。内存中会存在两个对象的内存,旧的对象和新写入的对象。注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)如果对象内存占用比较大有可能造成频繁的GC。
  • CopyOnWrite容器保证最终的数据一致性而不能保证数据的实时一致性

我是蚂蚁背大象,文章对你有帮助点赞关注我,文章有不正确的地方请您斧正留言评论~谢谢