目录
5.1 介绍
- 并发包中的并发 List 只有 CopyOnWriteArrayList。CopyOnWriteArrayList 是一个线程安全的 ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略,用来保证 list 的一致性,
- 但是获取—修改—写 入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁ReentrantLock,来保证在某个时间只有一个线程能对 list 数组进行修改。
- 但是在“获取指定位置元素”时没有加锁同步,所以 CopyOnWriteArrayList 提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对 list 的修改是不可见的。
5.2 主要方法源码解析
初始化
//无参构造函数,在内部创建了一个大小为 0 的 Object 数组作为 //array 的初始值。 public CopyOnWriteArrayList() { setArray(new Object[0]); } //有参构造函数。 //创建一个list,其内部元素是入参toCopyIn的副本 public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); } //入参为集合,将集合里面的元素复制到本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); }
5.2.2 添加元素
CopyOnWriteArrayList 中用来添加元素的函数有 add(E e) , add(int index, E element),addIfAbsent(E e) 和 addAllAbsent(Collection<? extends E> c) 等。
public boolean add(E e) {
//获取独占锁(1)
final ReentrantLock lock = this.lock;
lock.lock();
try {
//(2)获取array
Object[] elements = getArray();
//(3)复制array到新数组,添加元素到新数组
int len = elements.length;
//从这里可以知道新数组的大小是原来数组大小增加 1,所以 CopyOnWriteArrayList 是无界 list)
Object[] newElements = Arrays.copyOf(elements, len + 1);
//把新增的元素E e添加到新数组。
newElements[len] = e;
//(4)使用新数组替换添加前的数组
setArray(newElements);
return true;
} finally {
//(5)释放独占锁
lock.unlock();
}
}
由于加了锁,所以整个 add 过程是个原子性操作。需要注意的是,在添加元素时,首先 复制了一个快照 ,然 后在快照上进行添加,而不是直接在原来数组上进行。
5.2.3 获取指定位置元素
使用 E get(int index) 获取下标为 index 的元素,如果元素不存在则抛出IndexOutOfBoundsException 异常。public E get(int index) { //(1)先获取array 数组 return get(getArray(), index); } final Object[] getArray() { return array; } //(2)通过下标访问指定位置的元素 private E get(Object[] a, int index) { return (E) a[index]; }
这个过程没有加锁同步,会出现一些问题,以下具体分析:
由于执行步骤 A(获取array 数组 ) 和步骤 B (通过下标访问指定位置的元素)没有加锁,这就可能导致在线程 x 执行完步骤 A 后执行步骤 B 前,另外一个线程 y 进行了 remove 操作,线程y结束后, 这时线程 x 开始执行步骤 B ,步骤 B 操作的数组是线程 y 删除元素之前的数组,所以,虽然线程 y 已经删除了 index 处的元素,但是线程 x 的步骤 B 还是会返回index 处的元素,这其实就是 写时复制策略 产生的 弱一致性问题 。
5.2.4 修改指定元素
使用 E set(int index,E element )修改 list 中指定元素的值,如果指定位置的元素不存在则抛出 IndexOutOfBoundsException 异常public E set(int index, E element) { //获取了独占锁,从而阻止其他线程对 array 数组进行修改 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); //调用 get 方法获取指定位置的元素 E oldValue = get(elements, index); //如果指定位置的元素值与新值不一致则创建新数组并复制元素, //然后在新数组上修改指定位置的元素值并设置新数组到 array。 if (oldValue != element) { int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); newElements[index] = element; setArray(newElements); } else { //为了保证 volatile 语义,还是需要重新设置 array, setArray(elements); } return oldValue; } finally { lock.unlock(); } }
5.2.5 删除元素
删除 list 里面指定的元素 , 可以使用 E remove(int index) 、 boolean remove(Object o) 和boolean remove(Object o, Object[] snapshot, 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(); } }
首先获取独占锁以保证删除数据期间其他线程不能对 array 进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组之后使用新数组替换原来的数组,最后在返回前释放锁。
5.2.6 弱一致性的迭代器
迭代器使用:遍历列表元素。
{ CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>(); arrayList.add("hello"); arrayList.add("alibaba"); Iterator<String> itr = arrayList.iterator(); while(itr.hasNext()){ System.out.println(itr.next()); } }
弱一致性 是指返回迭代器后,其他线程对 list 的增删改对迭代器是不可见的。也就是说,如果在线程1中我最先获取了迭代器,线程2,3对数组元素进行增删改,然后执行完线程2,3后,线程1用获得的迭代器遍历数组,遍历的结果和初始数组一样,迭代器不会管(不可见)线程2,3对数组的更新后的新数组。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 E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; }
为什么说 snapshot 是 list 的快照呢?明明是指针传递的引用,而不是副本。那是因为如果在该线程使用返回的迭代器遍历元素的过程中,其他线程没有对 list 进行增删改,那么 snapshot 本身就是 list 的 array ,因为它们是引用关系。但是如果在遍历期间其他线程对该 list进行了增删改,那么 snapshot 就是快照了,因为增删改后 list 里面的数组被新数组替换了, 这时候老数组被 snapshot 引用。这也说明获取迭代器后,使用该迭代器元素时,其他线程对该 list 进行的增删改不可见,因为它们操作的是两个不同的数组,这就是弱一致性。所以说, 迭代器遍历的数组是一个快照。下面通过一个例子来演示多线程下迭代器的弱一致性的效果。可以自己运行查看结果public class copylist { private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>(); public static void main( String[] args ) throws InterruptedException { arrayList.add("hello"); arrayList.add("alibaba"); arrayList.add("welcome"); arrayList.add("to"); arrayList.add("hangzhou"); Thread threadOne = new Thread(new Runnable() { @Override public void run() { //修改list中下标为1的元素为baba arrayList.set(1, "baba"); //删除元素 arrayList.remove(2); arrayList.remove(3); } }); //保证在修改线程启动前获取迭代器 Iterator<String> itr = arrayList.iterator(); //启动线程 threadOne.start(); //等待子线程执行完毕 threadOne.join(); //迭代元素 while(itr.hasNext()){ System.out.println(itr.next()); } } }
主线程在子线程执行完毕后使用获取的迭代器遍历数组元素,从输出结果我们知道,在子线程里面进行的操作一个都没有生效,这就是迭代器弱一致性的体现。获取迭代器的操作必须在子线程操作之前进行。
CHAPTER-FIVE OVER.......
PROCESS--->5/11