线程安全的List(一):CopyOnWriteArrayList 源码解析

前言

  List 接口在 Java 里是非常常用的,常见的 List 接口实现类如 ArrayList,LinkedList,它们在各种场合都有着广泛的作用。然而这两个List 都是线程不安全的。本文将介绍一种线程安全的 ArrayList:CopyOnWriteArrayList,还是深入其源码进行分析和解释。看之前可以先看作者之前这篇博客:ArrayList源码学习(一):初始化,扩容以及增删改查,对 ArrayList 的基本操作和原理有一个认识。

基本结构和初始化

基本结构

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /**
     * The lock protecting all mutators.  (We have a mild preference
     * for builtin monitors over ReentrantLock when either will do.)
     */
     // 作者们的癖好
    final transient Object lock = new Object();

    // 存数据的数组
    private transient volatile Object[] array;
}
复制代码

  首先看 CopyOnWriteArrayList 的基本结构,代码如上。可以看出它就两个属性,一个是 Object 类型的 lock,用来当一个 Synchronized 的锁(我是JDK14版的,不同版本用的锁好像不一样),另一个是保存数据的 Object 类型的数组,名叫 array,它是 volatile 类型的,保证不同线程的可见性。

  这里面注释也说了,源码的作者们当Java内置的 Synchronized 锁和 ReentrantLock 都可以用的时候,他们更偏爱 Synchronized 锁。

初始化

// 无参构造
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
// 用集合来构造
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] es;
    if (c.getClass() == CopyOnWriteArrayList.class)
        es = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        es = c.toArray();
        if (c.getClass() != java.util.ArrayList.class)
            es = Arrays.copyOf(es, es.length, Object[].class);
    }
    setArray(es);
}
// 用数组来构造
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
// setArray 方法
final void setArray(Object[] a) {
    array = a;
}
// getArray 方法
final Object[] getArray() {
    return array;
}
复制代码

  三种初始化方法:

  • 无参的最简单,不用说;
  • 用集合来构造:如果此集合也为 CopyOnWriteArrayList,那直接让 es 指向对方 array 指向的数组,否则,如果是 ArrayList,运行es = c.toArray();,如果不是 ArrayList,执行es = Arrays.copyOf(es, es.length, Object[].class);,有人问了,这数组本来就是 es,再调用Arrays.copyOf(es, es.length, Object[].class)有啥意义?这个三参数版本的 Arrays.copyOf 的作用就是得到一个新数组,数组的元素由 es 复制而来,长度为 es.length,但是数组类型得为 Object 类型,所以还是有意义的,有些集合它 toArray 方法可能返回的不是 Object 类型的数组。这个我在 ArrayList 的博客里也说过。
  • 用数组来构造:三参数的 Arrays.copyOf 方法上面已经说过了,不解释。

写操作

  首先看看写操作,在 CopyOnWriteArrayList 里,只要对原数组有一点改动,都是写操作,都得加锁,哪怕只是 set 方法。(回想 ArrayList,它有个 modCount属性,标识数组有没有发生结构性改变,但是 ArrayList 的 set 方法却不会改变 modCount,所以 CopyOnWriteArrayList 对写真的很严格。)

set 方法

public E set(int index, E element) {
    // 抢锁
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);
        // 确实需要修改
        if (oldValue != element) {
            // 复制一份
            es = es.clone();
            // 修改
            es[index] = element;
        }
        setArray(es);
        return oldValue;
    }
}
复制代码

  可以看到,set 需要抢锁,毕竟是线程安全的集合,这样我觉得也是合理的。如果确实需要修改,那么es = es.clone();,让 es 指向克隆的数组,虽然名字还是 es,但现在已经有两份数组了,es 指向新的那份,接着进行修改。最后运行setArray(es);,让 array 指向这份新的数组。

  不难看出,仅仅是一个简单的 set 操作,却要复制整个数组,如果数组很大,效率会比较一般。

add 操作

add 一个元素

  这类方法有好几个版本,先看最简单的两个:

public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        // 克隆一个新数组,长度 + 1
        es = Arrays.copyOf(es, len + 1);
        // 尾部赋值
        es[len] = e;
        setArray(es);
        return true;
    }
}  

public void add(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException(outOfBounds(index, len));
        Object[] newElements;
        // 需要挪动的元素个数
        int numMoved = len - index;
        // 不用挪,直接尾部赋值
        if (numMoved == 0)
            newElements = Arrays.copyOf(es, len + 1);
        // 否则,需要挪动
        else {
            newElements = new Object[len + 1];
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index, newElements, index + 1,
                             numMoved);
        }
        // 赋值
        newElements[index] = element;
        setArray(newElements);
    }
}
复制代码

  第一个 add 方法是在尾部追加一个元素,直接克隆了一个长度 + 1 的数组,并进行赋值;第二个 add 方法其实和 ArrayList 的 add 差不多,也是先看有多少元素要往后移,接着进行相应的移动,然后在 index 处赋值。但是还是有不同的:

System.arraycopy(elementData, index,
                 elementData, index + 1,
                 s - index);
elementData[index] = element;
复制代码

  ArrayList 只会把 index 之后的元素后移,之前的不管。CopyOnWriteArrayList 却都要操作,这也很好理解,毕竟 CopyOnWriteArrayList 声明了一个新数组嘛,当然所有元素都要复制了。

  也可以看出来,CopyOnWriteArrayList 是不需要扩容的,为啥?ArrayList 需要扩容是因为起码在下次扩容之前,add 操作还是在 elemantData 这个数组上赋值的,它不会每次 add 都弄一个新数组出来,那么把容量多扩一点,也省得频繁扩容,频繁线性时间复杂度复制元素了;而 CopyOnWriteArrayList 每次 add 都新复制一个数组,有什么扩容的必要呢,即使扩了,下次还是复制一个新的出来,没有意义。所以 CopyOnWriteArrayList 每次 add 都弄了一个原长度 + 1 的新数组。

扫描二维码关注公众号,回复: 13556085 查看本文章

  接下来看难一点的 addIfAbsent:

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    // 运用逻辑表达式短路性质
    return indexOfRange(e, snapshot, 0, snapshot.length) < 0
        && addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
    synchronized (lock) {
        // 真正的添加元素,一堆代码,之后再细看
    }
}
复制代码

  上来是一个逻辑表达式的短路操作,先调用indexOfRange(e, snapshot, 0, snapshot.length),这个函数很简单:

private static int indexOfRange(Object o, Object[] es, int from, int to) {
    if (o == null) {
        for (int i = from; i < to; i++)
            if (es[i] == null)
                return i;
    } else {
        for (int i = from; i < to; i++)
            if (o.equals(es[i]))
                return i;
    }
    return -1;
}
复制代码

  没什么可解释的,从头到尾找元素!

  由于是 addIfAbsent,所以当indexOfRange(e, snapshot, 0, snapshot.length) < 0 && addIfAbsent(e, snapshot);前部分为真时,也就是确实没这个元素,才会进一步调用真正的 add,否则根本不会往下走了。这种短路操作在 Java 源码里太常见了。。。接下来看真正的 add:

private boolean addIfAbsent(E e, Object[] snapshot) {
    synchronized (lock) {
        // 新的 array 指向的数组
        Object[] current = getArray();
        int len = current.length;
        // 数组发生了改变,要重新判断
        if (snapshot != current) {
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i]
                    && Objects.equals(e, current[i]))
                    // 这个元素在 current 前半部分里出现了,返回false
                    return false;
            // // 这个元素在 current 后半部分里出现了,返回false
            if (indexOfRange(e, current, common, len) >= 0)
                    return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    }
}
复制代码

  那既然这个元素在数组里没有,那直接追加在尾部不就行了,为啥又要这一堆代码呢?那是因为一开始是在Object[] snapshot = getArray();的 snapshot 里进行找的,但这只是个快照,这个快照的内容在生成的一瞬间就确定了,不会改变。从上面一些写操作的分析,咱们也知道,CopyOnWriteArrayList 只要有写操作,就会改变数组的引用。所以可能你对 snapshot 进行 indexOfRange 的时间有点长,虽然你最后在 snopshot 里确实没找到这个元素,但可能在这段时间里,其它写操作改了 CopyOnWriteArrayList 的数组引用,当你找完 snopshot 并发元素不存在时,其它操作让 CopyOnWriteArrayList 的引用数组里有了这个元素,那么你再往里追加,不就错了嘛;或者你找完的时候,其它线程正在进行写操作,你走到这第一行代码synchronized (lock) {,连锁都抢不到,然后一直抢,等你抢到的时候,数组都不知道变成什么样了,,,是这个意思。

  解释了为啥有这一堆代码,咱们来仔细看看这个代码。取到了现在 CopyOnWriteArrayList 的快照 current 和它的长度 len。然后判断if (snapshot != current),如果两次快照一样,那太好了,不用担心我上面说的情况了,直接追加元素吧!

  否则,进入到 if 代码块里。int common = Math.min(snapshot.length, len);得到两个快照的最小长度,接着for (int i = 0; i < common; i++)循环遍历查找。这里查找的代码也很有意思:

if (current[i] != snapshot[i]
    && Objects.equals(e, current[i]))
    // 这个元素在 current 前半部分里出现了,返回false
    return false;  
复制代码

  这又是个短路操作。先看逻辑表达式前半部分:current[i] != snapshot[i],咱们走到这个函数的前提是什么?是在 snopshot 里没找到 e 这个元素,那么如果current[i] == snapshot[i],不用比了,current[i] 和 e 肯定也不相同,根据逻辑表达式的短路性质,也不会往下走了;只有当current[i] != snapshot[i]为真,才有把 current[i] 和 e 比较的必要,接下来进行Objects.equals(e, current[i]),如果这个也真,那没办法,看来新的快照确实有 e 这个元素,返回 false 吧 (T_T) 。至于为啥不直接比 current[i] 和 e,猜想可能是==操作比Objects.equals()更快吧。

  上面说的这个操作完了之后,还有if (indexOfRange(e, current, common, len) >= 0)这个判断,毕竟刚刚为防数组溢出,只比了int common = Math.min(snapshot.length, len);这么多元素嘛,可能还没比完呢。

  总之,比完了之后,如果在新快照里仍然没这个元素,那就追加此元素!

  有人问了,那我在 current 这个快照找的时候,期间就不会有别的写操作改了数组的引用吗?答案是不会,注意到private boolean addIfAbsent这个函数进来就是个抢锁的操作,所以倘若你得到了 current 这个快照,那么直到你离开这段代码前,都不会有别的线程可以进行写操作,改数组的引用。

add 一堆元素

  先看简单版本的:

public boolean addAll(Collection<? extends E> c) {
    // 取到对应的元素们
    Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
        ((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
    // 没什么好添加的
    if (cs.length == 0)
        return false;
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        Object[] newElements;
        // 如果原数组长度为0,且集合是 ArrayList 或 CopyOnWriteArrayList,直接赋值
        if (len == 0 && (c.getClass() == CopyOnWriteArrayList.class ||
                         c.getClass() == ArrayList.class)) {
            newElements = cs;
        // 否则,在尾部追加这些元素
        } else {
            newElements = Arrays.copyOf(es, len + cs.length);
            System.arraycopy(cs, 0, newElements, len, cs.length);
        }
        setArray(newElements);
        return true;
    }
}
复制代码

  这个代码也比较简单,就是经过一些判断之后(比方说待添加集合为空,直接返回 false),在原数组尾部追加集合里的元素,注释里写的很清楚了,不解释了。这个函数还有个这个版本的:public boolean addAll(int index, Collection<? extends E> c),从特定 index 后面添加集合里的元素,基本思想都一样,就是元素的挪来挪去,不想赘述了,大家自己找找看看,肯定能看懂。看下面这个难一点的:

public int addAllAbsent(Collection<? extends E> c) {
    Object[] cs = c.toArray();
    if (c.getClass() != ArrayList.class) {
        cs = cs.clone();
    }
    // 集合为空,不需要添加,直接返回
    if (cs.length == 0)
        return 0;
    synchronized (lock) {
        // 一堆复杂代码,之后解释
    }
}
复制代码

  这个函数是如果数组里没有集合里的元素,才进行添加,有就不加了。然后先拿到这个集合的元素的数组。这块有这个判断:

if (c.getClass() != ArrayList.class) {
    cs = cs.clone();
}
复制代码

  为什么要这样,我也不太清楚,猜想可能是有些集合的 toArray 返回的是自己的数组本身,怕污染了集合的原数组?然后如果集合为空,那直接返回就行了,接下来看 synchronized 代码块里的代码,这是真正进行添加了:

synchronized (lock) {
    // 拿到自己的快照,这之后不会有线程改变此快照
    Object[] es = getArray();
    int len = es.length;
    int added = 0;
    // 进行添加
    for (int i = 0; i < cs.length; ++i) {
        Object e = cs[i];
        // e 不能在 CopyOnWriteArrayList 里出现
        // 同时也不能添加重复的 e
        if (indexOfRange(e, es, 0, len) < 0 &&
            indexOfRange(e, cs, 0, added) < 0)
            cs[added++] = e;
    }
    // 有符合条件的元素需要添加
    if (added > 0) {
        // 尾部追加
        Object[] newElements = Arrays.copyOf(es, len + added);
        System.arraycopy(cs, 0, newElements, len, added);
        setArray(newElements);
    }
    return added;
}
复制代码

  这个代码主要看 for 循环里的代码,Object e = cs[i];之后是一个 if,第一个条件indexOfRange(e, es, 0, len) < 0很好理解,这个元素 e,我们的数组里确实不存在,那么第二个条件呢?indexOfRange(e, cs, 0, added) < 0,注意到 added 初始化为 0,所以当第一个不存在于 CopyOnWriteArrayList 出现的时候,第二个条件肯定为真的,于是 added 变为了 1,那么再来第二个满足了第一个条件的 e 出现时,走一遍第二个条件,意思就是这个 e 不仅不能在 CopyOnWriteArrayList 里出现,也不能在cs[0:added]里出现,就是不能重复添加元素。这也符合addAllAbsent的语义,同样的元素如果添加了两次,和这个 Absent 可能有点冲突吧。

  最后就是在尾部追加cs[0:added]的元素了。

remove 操作

remove 一个元素

  最简单的按下标删:

public E remove(int index) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        E oldValue = elementAt(es, index);
        int numMoved = len - index - 1;
        Object[] newElements;
        // 删的是最后一个元素
        if (numMoved == 0)
            newElements = Arrays.copyOf(es, len - 1);
        // 否则,进行元素的复制,挪动
        else {
            newElements = new Object[len - 1];
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index + 1, newElements, index,
                             numMoved);
        }
        setArray(newElements);
        return oldValue;
    }
}
复制代码

  就是先int numMoved = len - index - 1;计算有多少元素要往前挪,如果没有元素往前挪,说明删的最后一个元素,直接newElements = Arrays.copyOf(es, len - 1);,否则,先把 0 - index 处的元素复制到 newElements,再把 index 后的元素往前挪动一步,复制到 newElements 里。

  再看按元素删:

// 先找一下元素有没有
public boolean remove(Object o) {
    Object[] snapshot = getArray();
    int index = indexOfRange(o, snapshot, 0, snapshot.length);
    // 在快照里找到了,再真正 remove
    return index >= 0 && remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
    synchronized (lock) {
        // 一堆操作,之后细说
    }
}
复制代码

  和 addIfAbsent 一样,同样是先在快照里找,找到了才进行下一步,return index >= 0 && remove(o, snapshot, index);,仍然是短路操作。接下来看 synchronized 代码块的内容:

synchronized (lock) {
    // 拿到目前的快照
    Object[] current = getArray();
    int len = current.length;
    if (snapshot != current) findIndex: {
        int prefix = Math.min(index, len);
        // 前半部分找
        for (int i = 0; i < prefix; i++) {
            if (current[i] != snapshot[i]
                && Objects.equals(o, current[i])) {
                index = i;
                break findIndex;
            }
        }
        // 一些边界判断
        if (index >= len)
            return false;
        if (current[index] == o)
            break findIndex;
        // 后半部分找(如果有的话)
        index = indexOfRange(o, current, index, len);
        if (index < 0)
            return false;
    }
    // 删除 index 处的元素的常规操作
    Object[] newElements = new Object[len - 1];
    System.arraycopy(current, 0, newElements, 0, index);
    System.arraycopy(current, index + 1,
                     newElements, index,
                     len - index - 1);
    setArray(newElements);
    return true;
}
复制代码

  if (snapshot != current)如果为假,那直接进行删除就行了。

  否则,需要走 if 里那一堆代码。和 addIfAbsent 一样的思想,先int prefix = Math.min(index, len);,从 0 到 prefix 找,因为 prefix 肯定小于等于 index,而 index 又是上一个快照第一次找到 o 的索引,所以 0 到 prefix 的寻找可以用if (current[i] != snapshot[i] && Objects.equals(o, current[i])) { 来优化了。只有current[i] != snapshot[i]时,才有进一步Objects.equals(o, current[i])的必要。如果找到了满足Objects.equals(o, current[i])的 i,将它赋值给 index,然后 break 跳出来;

  如果 0 到 prefix 找完了也没找到,出来这个 for 循环之后,如果 index >= len,说明 0 到 len 都没有找到,返回 false;否则,index < len,还有 index 到 len 没有找,先if (current[index] == o),进行一个快速匹配,如果这个找到了,直接 break 跳出来,否则,老老实实index = indexOfRange(o, current, index, len);在后半部分找,如果后半部分也没找到,即if (index < 0),那返回 false。

  总之,最后找到了,再运行那一段删除 index 处元素的代码即可。

remove 一堆元素

  首先还是看简单的版本:

void removeRange(int fromIndex, int toIndex) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        // 下标检查
        if (fromIndex < 0 || toIndex > len || toIndex < fromIndex)
            throw new IndexOutOfBoundsException();
        // 新长度
        int newlen = len - (toIndex - fromIndex);
        // 多少元素往前挪
        int numMoved = len - toIndex;
        // 没有元素需要挪
        if (numMoved == 0)
            setArray(Arrays.copyOf(es, newlen));
        // 进行一系列挪动
        else {
            Object[] newElements = new Object[newlen];
            System.arraycopy(es, 0, newElements, 0, fromIndex);
            System.arraycopy(es, toIndex, newElements,
                             fromIndex, numMoved);
            setArray(newElements);
        }
    }
}
复制代码

  这个代码先检查 fromIndex 和 toIndex 的下标是否合法,然后计算新长度和有多少元素需要前挪,如果没有元素往前挪,直接setArray(Arrays.copyOf(es, newlen));即可,否则,先复制 0 到 fromIndex 的元素,再复制 toIndex 到 len 的元素。反正就是挪动 + 复制。再看难一点的:

// 删除位于集合 c 里的数组里的元素
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return bulkRemove(e -> c.contains(e));
}
// 保留位于集合 c 里的数组里的元素,删除集合 c 里没有的元素
public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return bulkRemove(e -> !c.contains(e));
}
private boolean bulkRemove(Predicate<? super E> filter) {
    synchronized (lock) {
        return bulkRemove(filter, 0, getArray().length);
    }
}
复制代码

  这里用 lambda 表达式表示了删除的条件,bulkRemove 里是一个 Predicate 的函数式接口。看一下这个三参数版的 bulkRemove。

  算了,这个代码真的很长,我就不复制了,这是一个用 long 类型的数组和位操作巧妙的进行删除的方法,不是很容易看懂,之前我的这篇博客很详细的介绍了这种方法:ArrayList源码学习(三):removeIf,我就不在这复制黏贴了,反正这个思想挺有意思的。

  写操作到这就告一段落吧,还有什么 replace 方法,都太简单了,没什么说的意义。总结下写操作就是:只要改了原数组了,必定会触发数组的所有元素的复制,以及 setArray 方法,哪怕是最简单的 set 方法。

读操作

  读操作就简单很多了,这里放上几个常见的读操作代码:

// get 方法
public E get(int index) {
    return elementAt(getArray(), index);
}
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}
// 根据元素找下标
public int indexOf(Object o) {
    Object[] es = getArray();
    return indexOfRange(o, es, 0, es.length);
}
private static int indexOfRange(Object o, Object[] es, int from, int to) {
    if (o == null) {
        for (int i = from; i < to; i++)
            if (es[i] == null)
                return i;
    } else {
        for (int i = from; i < to; i++)
            if (o.equals(es[i]))
                return i;
    }
    return -1;
}
复制代码

  可以看到,读操作不需要加锁,也不用复制数组元素,读上来会调用个 getArray 方法,拿到这个快照,对这个快照进行查找。

CopyOnWriteArrayList 读写小结

  看完了大部分的 CopyOnWriteArrayList 的读写操作的代码,咱们可以总结下它的读写的特点:

  • 写操作要加锁,抢锁,同时只有一个线程能写,并且写的时候要复制整个数组的元素
  • 读操作不用加锁,会调用 getArray 方法得到快照,对此快照进行查找
  • 写写互斥(因为要抢锁),读读不互斥,读写不互斥
  • 读操作得到的结果可能不是最新的,因为只是个快照的内容。读操作可以得到的结果在快照生成那一瞬间就确定了
  • 写操作时,并不会直接对现有的数组/快照进行修改,所以读操作不用担心读着读着,快照被其它写操作改了
  • 写完之后,会 setArray 改变当前 CopyOnWriteArrayList 的 array 数组

迭代器

初始化和基本结构

  这是几个调迭代器的函数,都是传给 COWIterator 方法一个 getArray 得到的快照,和开始的 index。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}  
public ListIterator<E> listIterator() {
    return new COWIterator<E>(getArray(), 0);
}
public ListIterator<E> listIterator(int index) {
    Object[] es = getArray();
    int len = es.length;
    if (index < 0 || index > len)
        throw new IndexOutOfBoundsException(outOfBounds(index, len));

    return new COWIterator<E>(es, index);
}
复制代码

  下面看看它的基本结构以及初始化:

static final class COWIterator<E> implements ListIterator<E> {
    // 迭代器对应的快照
    private final Object[] snapshot;
    // 将访问的数组索引
    private int cursor;
    // 初始化
    COWIterator(Object[] es, int initialCursor) {
        cursor = initialCursor;
        snapshot = es;
    }
// 其它操作代码
}
复制代码

  不难看出,迭代器本身维护一个 CopyOnWriteArrayList 的快照 snapshot,以及 cursor 作为下一个要访问的数组索引。

next 和 previous 方法

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++];
}

@SuppressWarnings("unchecked")
public E previous() {
    if (! hasPrevious())
        throw new NoSuchElementException();
    return (E) snapshot[--cursor];
}
复制代码

  反正每次调用前都看看if (! hasNext())或者if (! hasPrevious()),代码层面实在是很简单,不说了。

写操作

public void remove() {
    throw new UnsupportedOperationException();
}
public void set(E e) {
    throw new UnsupportedOperationException();
}
public void add(E e) {
    throw new UnsupportedOperationException();
}
复制代码

  看来作者 Doug Lea 大佬不愿意让 CopyOnWriteArrayList 的迭代器有修改的功能。。。

迭代器小结

  迭代器也是通过拿到 CopyOnWriteArrayList 某一瞬间的快照,对此快照进行操作。

写时复制思想

Linux 中的写时复制

  当初上操作系统课时,记得老师布置过这种父子进程创建和通信的作业。在Linux程序中,fork()会产生一个和父进程完全相同的子进程,出于效率考虑,Linux 没有逐个复制父进程的数据给子进程,而是引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

写时复制思想的应用

Redis 的 RDB 持久化

  以我目前的水平来说,除了 Java 这个 CopyOnWriteArrayList 和 CopyOnWriteArraySet(基于CopyOnWriteArrayList,代码很少),这块应用还知道的就是 Redis 的 RDB 持久化了。

  RDB 持久化有两种,一种调用 save,由主进程执行持久化,会导致单进程的 Redis 阻塞很久,一般不推荐;另一种就是 调用 bgsave,fork 一个子进程来进行持久化,由于 RDB 持久化属于只读操作,不会修改原数据,所以也比较适用写时复制思想。RDB 可以得到的快照在子进程创建出来的瞬间就确定了。

总结

  本文主要对 CopyOnWriteArrayList 的主要源码进行了解析,由于工作量比较大,有些源码如 subList 没有讲,大家感兴趣可以自己去看看,比较简单。

猜你喜欢

转载自juejin.im/post/7040804870370099208