此文为读书笔记,欢迎评论,讨论问题,共同进步!
介绍
并发包中的并发List只有CopyOnWriteArrayList。CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。类图结构如图:
在 CopyOnWriteArrayList 的类图中,每个CopyOnWriteArrayList对象里面有一个 array数组对象用来存放具体元素,ReentrantLock独占锁对象用来保证同时只有一个线程对 array 进行修改。这里只要记得 ReentrantLock是独占锁,同时只有一个线程可以获取。
设计一个线程安全的 list 所要考虑的问题:
- 何时初始化 list,初始化的 list 元素个数为多少,list 是有限大小吗?
- 如何保证线程安全,比如多个线程进行读写时如何保证是线程安全的?
- 如何保证使用迭代器遍历 list 时的数据一致性?
带着上面的问题往下看。
主要方法源码解析
初始化
// 无参构造函数,内部创建了一个大小为0的Object数组作为 array 的初始值
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 入参为集合,将集合里面的元素复制到本list
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);
}
// 创建 list ,其内部元素是入参toCopyin 的副本
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
final void setArray(Object[] a) {
array = a;
}
private transient volatile Object[] array;
添加元素
- boolean add(E e)
- void add(int index, E element)
一共两个添加方法,源码也比较简单:
public boolean add(E e) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取 内部 array
Object[] elements = getArray();
int len = elements.length;
// 复制 array到新数组 添加元素到新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 用新数组替换旧的数组
setArray(newElements);
return true;
} finally {
// 释放独占锁
lock.unlock();
}
}
public void add(int index, E element) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取 内部 array
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
// 新数组
Object[] newElements;
// 如果当前index等于源数组的长度,说明index下标添加到源数组的尾部
int numMoved = len - index;
if (numMoved == 0)
// 和boolean add(E e)一致,当前元素要添加到尾部
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
// System.arraycopy(源数组, 源数组起始位置, 目标数组, 在目标数据中的起始位置, 要复制的数组元素的数量);
// 从原数组中复制下标0到index-1的数据到新数组
System.arraycopy(elements, 0, newElements, 0, index);
// 从原数组中复制下标index到(len - index)就是到最后 的数据到新数组
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
// 新数组中空出的index位置的数据添加数据
newElements[index] = element;
// 用新数组替换旧的数组
setArray(newElements);
} finally {
// 释放独占锁
lock.unlock();
}
}
获取指定位置元素
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
上面源码很简单,可以看出获取指定位置的元素分两步:
- 第一步:获取内部数组array
- 第二步:获取数组中指定位置的元素
上面代码中并没加锁,会有写时复制策略产生的弱一致性问题:
比如数组中有1,2,3 元素,A线程get(0)只执行到第一步,这是B线程remove(0)了,这时A线程执行完了第二步,返回的还是1。
修改指定元素
public E set(int index, E element) {
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 {
// 不是完全禁止操作;确保可变的写语义
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
如上代码首先获取了独占锁,从而阻止其他线程对 aray数组进行修改,然后获取当前数组,并调用 get方法获取指定位置的元素,如果指定位置的元素值与新值不一致则创建新数组并复制元素,然后在新数组上修改指定位置的元素值并设置新数组到aray。如果指定位置的元素值与新值一样,则为了保证volatile语义,还是需要重新设置aray,虽然 array 的内容并没有改变。
删除元素
删除list里面指定的元素有三个方法:
- E remove(int index)
- boolean remove(Object o)
- boolean remove(Object o, Object[] snapshot, int index)
下面说下E remove(int index)
方法,其他两个类似,不做赘述。
public E remove(int index) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取数组
Object[] elements = getArray();
int len = elements.length;
// 获取指定元素
E oldValue = get(elements, index);
int numMoved = len - index - 1;
// 如果要删除的是最后一个元素
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
// 分两次复制删除后剩余的元素到新数纽
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 使用新数组替换老数组
setArray(newElements);
}
return oldValue;
} finally {
// 释放锁
lock.unlock();
}
}
弱一致性的迭代器
遍历列表元素可以使用迭代器。先看一个例子:如何使用迭代器。
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList();
arrayList.add("hello");
arrayList.add("world");
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
hello
world
迭代器的hasNext方法用于判断列表中是否还有元素,next方法则具体返回元素。
下面来看 CopyOnWriteArrayList 中迭代器的弱一致性是怎么回事,所谓弱一致性是指返回迭代器后,其他线程对 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;
}
...
// 获取元素
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
...
}
在如上代码中,当调用iterator()方法获取迭代器时实际上会返回一个COWIterator对象,COWIterator对象的snapshot变量保存了当前list的内容,cursor是遍历list时数据的下标。
为什么说snapshot是 list 的快照呢?明明是指针传递的引用啊,而不是副本。如果在该线程使用返回的迭代器遍历元素的过程中,其他线程没有对list进行增删改,那么 snapshot本身就是 list 的 array,因为它们是引用关系。但是如果在遍历期间其他线程对该 list 进行了增删改,那么snapshot就是快照了,因为增删改后list里面的数组被新数组替换了,这时候老数组被snapshot引用。这也说明获取迭代器后,使用该迭代器元素时,其他线程对该 list 进行的增删改不可见,因为它们操作的是两个不同的数组,这就是弱一致性。
下面通过一个例子来演示多线程下迭代器的弱一致性的效果。
public class CopyListTest {
private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
public static void main(String[] args) throws InterruptedException {
arrayList.add("I ");
arrayList.add("love ");
arrayList.add("you ");
arrayList.add("china!");
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
arrayList.set(0, "We ");
arrayList.remove(3);
}
});
// 在线程启动之前获取迭代器
Iterator<String> iterator = arrayList.iterator();
// 启动线程
threadA.start();
// 等子线程执行结束
threadA.join();
// 迭代元素
while (iterator.hasNext()) {
System.out.print(iterator.next());
}
}
}
I love you china!
从输出结果我们知道,在子线程里面进行的操作一个都没有生效,这就是迭代器弱一致性的体现。需要注意的是,获取迭代器的操作必须在子线程操作之前进行。
总结
CopyOnWriteArrayList 使用写时复制的策略来保证 list 的一致性,而获取一修改—写入
三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对 list 数组进行修改。另外 CopyOnWriteArayList 提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照。另外,CopyOnWriteArraySet 的底层就是使用它实现的。