CopyOnWriteArrayList学习笔记
Posted 花花young
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CopyOnWriteArrayList学习笔记相关的知识,希望对你有一定的参考价值。
前言
并发包中的并发List只有CopyOnWriteArrayList。CopyOnArrayList是一个线程安全的ArrayList,对其进行修改的操作都是在底层的一个复制的数组上进行的,也就是使用了写时复制策略。
CopyOnWriteArrayList源码解析
初始化
public CopyOnWriteArrayList()
setArray(new Object[0]);
无参构造函数内部创建了一个大小为0的Object数组作为array的初始值。然后看下有参构造函数:
// 如果是普通的 list ,都会重新拷贝一份,不会影响原来的 list
public CopyOnWriteArrayList(Collection<? extends E> c)
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else
elements = c.toArray();
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
setArray(elements);
//创建list,其内部元素是入参toCopyIn的副本
public CopyOnWriteArrayList(E[] toCopyIn)
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
添加元素
尾部添加
public boolean add(E e)
//(1)获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try
// (2)得到所有的原数组
Object[] elements = getArray();
int len = elements.length;
//(3)拷贝到新数组里面,新数组的长度是 + 1 的
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新数组中进行赋值,新元素直接放在数组的尾部
newElements[len] = e;
//(4)替换原来的数组
setArray(newElements);
return true;
finally
//(5)释放独占锁
lock.unlock();
从上get到:
- 调用add方法只有一个线程会获取到该锁,其它线程会被阻塞挂起直到锁被释放
- 新数组的大小是原来数组大小增加1,所以CopyOnWriteArrayList是无界List
- 在添加元素时,首先复制了一个快照,然后在快照上进行添加,而不是直接在原来数组上进行
add指定位置添加元素:
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
// 如果要插入的位置在数组的中间,就需要拷贝 2 次
// 第一次从 0 拷贝到 index。
// 第二次从 index+1 拷贝到末尾。
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
// index 索引位置的值是空的,直接赋值即可。
newElements[index] = element;
setArray(newElements);
finally
lock.unlock();
从上get到:
- 当插入的位置正好位于末尾时,只需要拷贝一次,当插入的位置处于中间时,此时我们会把原数组一分为二,进行两次拷贝操作。
删除操作和add方法类似,这里看下批量删除操作:
public boolean removeAll(Collection<?> c)
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try
Object[] elements = getArray();
int len = elements.length;
// 说明数组有值
if (len != 0)
// newlen 表示新数组的索引位置
int newlen = 0;
Object[] temp = new Object[len];
// 循环,把不包含在 c 里面的元素,放到新数组中
for (int i = 0; i < len; ++i)
Object element = elements[i];
// 不包含在 c 中的元素,从 0 开始放到新数组中
if (!c.contains(element))
temp[newlen++] = element;
// 拷贝新数组,变相的删除了不包含在 c 中的元素
if (newlen != len)
setArray(Arrays.copyOf(temp, newlen));
return true;
return false;
finally
lock.unlock();
从上get到:
批量删除操作并不会直接对数组中的元素进行挨个删除,而是先将数组中的值进行循环判断,把我们不需要删除的数组放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。这样提升了性能
迭代器的弱一致性
弱一致性是指返回迭代器后,其它线程对list的增删改对迭代器是不可见的。
public Iterator<E> iterator()
return new COWIterator<E>(getArray(), 0);
static final class COWIterator<E> implements ListIterator<E>
//array的快照
private final Object[] snapshot;
//数组下标
private int cursor;
private COWIterator(Object[] elements, int initialCursor)
cursor = initialCursor;
snapshot = elements;
public boolean hasNext()
return cursor < snapshot.length;
public boolean hasPrevious()
return cursor > 0;
@SuppressWarnings("unchecked")
public E next()
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
如果没有其它线程对list进行增删改,那么snapshot本身就是list的array,因为他们是引用关系。但是如果在遍历期间其它线程对该list进行增删改,那么snapshot就是快照了,因为在增删改之后list里面的数组就被新数组替换了,它们操作的是两个不同的数组,这就是弱一致性。
总结
- CopyOnWriteArrayList仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致
- CopyOnWriteArrayList迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照行增删改是没有意义的
以上是关于CopyOnWriteArrayList学习笔记的主要内容,如果未能解决你的问题,请参考以下文章
Java源码学习(JDK 11)——java.util.concurrent.CopyOnWriteArrayList
Java源码学习(JDK 11)——java.util.concurrent.CopyOnWriteArrayList