天天看点

《Java并发编程之美》阅读笔记(五)Java并发包中并发List解析5.1 介绍5.2 主要方法源码解析

5.1 介绍

在Java并发包中的并发List集合只有CopyOnWriteArrayList。CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了读写时复制策略。该类的类图如下:

《Java并发编程之美》阅读笔记(五)Java并发包中并发List解析5.1 介绍5.2 主要方法源码解析

如图,CopyOnWriteArrayList内部维护一个array数组对象来存放具体的数据,ReentrantLock是一个独占锁,保证只有一个线程能操作该对array数组进行操作。

带着以下三个问题学习下边的内容:

  • 何时初始化List?初始化的List中元素个数为多少?List的大小有限制吗?
  • 如何保证线程安全,比如多个线程进行读写操作时如何保证线程是安全的?
  • 如何保证使用迭代器遍历list时的数据一致性?

下边学习源码是如何设计的。

5.2 主要方法源码解析

5.2.1 初始化

首先看无参构造,它传参是一个大小为0的Object类型数组作为array的初始值。

《Java并发编程之美》阅读笔记(五)Java并发包中并发List解析5.1 介绍5.2 主要方法源码解析

再看有参构造函数:

《Java并发编程之美》阅读笔记(五)Java并发包中并发List解析5.1 介绍5.2 主要方法源码解析

5.2.2 添加元素

CopyOnWriteArrayList用来添加元素的函数有add(E e)、add(int index , E element)、addIfAbsent(E e) 、addAllAbsent(Collection<? extends E>C)等。他们的原理类似,下边以add(E e)为例进行讲解。

《Java并发编程之美》阅读笔记(五)Java并发包中并发List解析5.1 介绍5.2 主要方法源码解析

(1)调用add方法的线程会先获得一个独占锁,如果有多个线程尝试调用add方法则只有一个线程能获得独占锁,其他线程暂时挂起。

(2)获得array的快照,实际是将array的元素复制进了一个Object数组中。

(3)复制array的快照到新数组中(这里可以看到新数组的大小是原array长度+1,所以list的大小是无界的)。然后把新元素添加到新数组中。

然后(4)将新数组替换掉旧的array数组。

(5)在方法返回前释放独占锁。

注意,(1)(2)(3)上的操作都是对array的快照进行的,并没有直接操作array数组。这就是读写时复制策略。

5.2.3 获取指定位置的元素

使用 E get(int index) 方法可以获得指定位置的元素,如果指定位置不存在元素则抛出IndexOutOfBoundsException异常。

《Java并发编程之美》阅读笔记(五)Java并发包中并发List解析5.1 介绍5.2 主要方法源码解析

当调用 E get(int index)方法时,内部执行分为两步:

1、首先调用getArray()方法获取array数组

2、再调用 E get(Object[] a , int index) 方法,获得array数组内指定位置的元素

需要注意的是:该方法的一系列操作并没有对array加锁,所以再get指定位置的元素时,array可能正在被修改。修改的操作会先修改array的快照,再修改array数组,读array和写快照的过程对array不加锁。那么假如修改和get的是同一个位置的元素,可能会刚get到指定元素,该元素就被就修改了(可能是更新或删除)。此时就出现了数据一致性的问题。这体现的是读写复制策略的弱一致性。

5.2.4 修改指定位置的元素

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数组。

5.2.5 删除指定的元素

删除指定的元素可以使用 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)两步就是将原数组内除指定位置之外的元素都复制到了新数组中。

5.2.6 弱一致性的迭代器

先看一下使用迭代器遍历集合:

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<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
           

迭代器的使用比较简单。

迭代器的弱一致性是

继续阅读