在Java并发包中的并发List集合只有CopyOnWriteArrayList。CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了读写时复制策略。该类的类图如下:
如图,CopyOnWriteArrayList内部维护一个array数组对象来存放具体的数据,ReentrantLock是一个独占锁,保证只有一个线程能操作该对array数组进行操作。
带着以下三个问题学习下边的内容:
下边学习源码是如何设计的。
首先看无参构造,它传参是一个大小为0的Object类型数组作为array的初始值。
再看有参构造函数:
CopyOnWriteArrayList用来添加元素的函数有add(E e)、add(int index , E element)、addIfAbsent(E e) 、addAllAbsent(CollectionC)等。他们的原理类似,下边以add(E e)为例进行讲解。
(1)调用add方法的线程会先获得一个独占锁,如果有多个线程尝试调用add方法则只有一个线程能获得独占锁,其他线程暂时挂起。
(2)获得array的快照,实际是将array的元素复制进了一个Object数组中。
(3)复制array的快照到新数组中(这里可以看到新数组的大小是原array长度+1,所以list的大小是无界的)。然后把新元素添加到新数组中。
然后(4)将新数组替换掉旧的array数组。
(5)在方法返回前释放独占锁。
注意,(1)(2)(3)上的操作都是对array的快照进行的,并没有直接操作array数组。这就是读写时复制策略。
使用 E get(int index) 方法可以获得指定位置的元素,如果指定位置不存在元素则抛出IndexOutOfBoundsException异常。
当调用 E get(int index)方法时,内部执行分为两步:
1、首先调用getArray()方法获取array数组
2、再调用 E get(Object[] a , int index) 方法,获得array数组内指定位置的元素
需要注意的是:该方法的一系列操作并没有对array加锁,所以再get指定位置的元素时,array可能正在被修改。修改的操作会先修改array的快照,再修改array数组,读array和写快照的过程对array不加锁。那么假如修改和get的是同一个位置的元素,可能会刚get到指定元素,该元素就被就修改了(可能是更新或删除)。此时就出现了数据一致性的问题。这体现的是读写复制策略的弱一致性。
public E set(int index, E element) {
final ReentrantLock lock = this.lock;//(1)
lock.lock();
try {
Object[] elements = getArray();//(2)
E oldValue = get(elements, index);//(3)
if (oldValue != element) {//(4)
//(6)
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {//(5)
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
首先(1)调用E set(int index , E element)的线程会获得ReentrantLock独占锁,保证只有一个线程能调用该方法成功,其他调用该方法的线程暂时挂起。
(2)获得array的快照。
(3)获得指定位置现有的值。
(4)判断指定位置的现有值与想更新的值是否相等,如果相等则(5)不需要更新,为了保证volatile语义,将array数组设置为快照数组的内容(即不变)。若不相等,则(6)创建一个大小与原数组相等的数组,并将快照内的数据复制进新数组,然后修改新数组中指定位置的元素,最后更新array数组。
删除指定的元素可以使用 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;//(1)获取独占锁
lock.lock();
try {
Object[] elements = getArray();//(2)获得array的快照
int len = elements.length;
E oldValue = get(elements, index);//(3)获得要删除的具体值
int numMoved = len - index - 1;//
if (numMoved == 0) //(4)说明删除的是最后一个元素
setArray(Arrays.copyOf(elements, len - 1));//(5)复制除最后一个元素之外的元素并替换array
else {
Object[] newElements = new Object[len - 1];//(6)创建新数组,长度为原长度-1
System.arraycopy(elements, 0, newElements, 0, index);//(7)
System.arraycopy(elements, index + 1, newElements, index,
numMoved);//(8)
setArray(newElements);//(9)更新array的内容为新数组内容
}
return oldValue;
} finally {
lock.unlock();
}
}
其中System.arraycopy(原数组,开始复制的位置,目标数组,开始放置的位置,复制的长度)
(7)(8)两步就是将原数组内除指定位置之外的元素都复制到了新数组中。
先看一下使用迭代器遍历集合:
package com.learnThread.demo.part3;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author:tongys
* @createTime:2020/1/10 15:52
*/
public class TestIterator {
public static void main(String[] args) {
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
迭代器的使用比较简单。
迭代器的弱一致性是