Java1.8-CopyOnWriteArrayList源码解析(三)

一、概述

  CopyOnWriteArrayList是对List进行操作的一个线程安全的类,是ArrayList的线程安全的变体,Copy-On-Write一般翻译为“写入时复制”,很明显,该类实现线程安全的方式是通过复制来实现的,也就是说,首先提供一个不可变的对象,然后在每次修改时,都会创建并生成一个新的不可变的对象,从而实现可变性。
  自然的,这种实现方式也有它的缺点,因为每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模比较大时,所以只有当查询操作远远多于修改操作时,才建议使用该类。

本系列源码全是基于JDK 1.8的。

二、CopyOnWriteArrayList

1. 继承结构及构造方法
public class CopyOnWriteArrayList
    implements List, RandomAccess, Cloneable, java.io.Serializable {

继承结构比较常规,可以随机访问(RandomAccess),可以复制(Cloneable),可以序列化(Serializable ),然后来看下构造方法:

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

public CopyOnWriteArrayList(Collection c) {
    Object[] elements;
    // 类型是否相同
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList)c).getArray();
    else {
        // 不同,先转为数组,然后判断是否为Object数组类型
        // 如果不是Object数组类型,就转为该类型
        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);
}

public CopyOnWriteArrayList(E[] toCopyIn) {
    // 转化为Object数组类型,然后设置数组
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}


final void setArray(Object[] a) {
    array = a;
}

由于该类底层同样是基于数组实现的,所以构造方法的操作也是围绕着数组进行的,该类总共提供了三个构造方法,其实都很简单,源码中也简单添加了注释,这里就不多说了。

2. 常用属性

这里也来简单看下用到的一些属性:

/** 使用可重入锁保证线程安全 */
final transient ReentrantLock lock = new ReentrantLock();

/** 数组,用于存放具体的元素 */
private transient volatile Object[] array;

/** Unsafe变量,用于实现原子性操作 */
private static final sun.misc.Unsafe UNSAFE;
private static final long lockOffset;
3. 相关内部类
3.1 COWIterator类
static final class COWIterator implements ListIterator {
    /** 基础数组array的快照 */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    /** 可以翻译为游标 */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

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

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor-1;
    }

    /** 不支持add,set,remove操作 */
    public void remove() {
        throw new UnsupportedOperationException();
    }
    public void set(E e) {
        throw new UnsupportedOperationException();
    }
    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void forEachRemaining(Consumer action) {
        Objects.requireNonNull(action);
        Object[] elements = snapshot;
        final int size = elements.length;
        for (int i = cursor; i < size; i++) {
            @SuppressWarnings("unchecked") E e = (E) elements[i];
            action.accept(e);
        }
        cursor = size;
    }
}

该类用于表示迭代器,提供了一个基础数组array的快照,该迭代器保留了一个指向底层基础数组的引用,该数组不会被修改,因此在对其进行操作的时候只需确保数组内容的可见性,这样多个线程可以同时对这个容器进行迭代,而不会发生冲突。

4. 方法

接下来,我们来看一下它常用的一些方法,先看下add方法:

4.1 add方法
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 进行加锁操作
    lock.lock();
    try {
        // 获取基础数组array
        Object[] elements = getArray();
        int len = elements.length;
        // 复制数组,长度加1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 把元素存进去
        newElements[len] = e;
        // 更新数组
        setArray(newElements);
        return true;
    } finally {
        // 结束,释放锁
        lock.unlock();
    }
}

add方法很简单:

  • 首先,获取锁,获取数组及数组长度;
  • 其次,在原数组基础上复制一个新的数组,新的数组比原数组长度多1;
  • 然后把新的元素添加到数组的尾部;
  • 最后把该数组赋值给基础数组array;

而add的另一个重载方法就稍微复杂一点点:

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 判断索引是否合法
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        // 构造新数组
        Object[] newElements;
        // 计算元素要保存的位置的下标值
        int numMoved = len - index;
        // 如果元素是数组最后一个元素,操作和上面描述的一个参数的add方法操作相同
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // 否则,构造新的数组,长度是 基础数组长度加1
            newElements = new Object[len + 1];
            // 进行两次复制,先复制索引前的元素,再复制索引后的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        // 将新元素存进去,并更新基础数组
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

其实该方法也比较简单,主要是判断了下index的大小,然后根据index的大小进行相应的复制操作,这里注释已经很清晰了,这里就步多说了。

4.2 addIfAbsent方法

该方法表示添加的时候如果数组中不存在,则进行添加;如果存在,则不添加,直接返回:

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        // 如果快照和当前数组不相等,说明数组发生了修改
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            // 这个时候取数组长度较小的值,这里是一个优化操作
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                // 如果当前数组元素与快照数组元素不相等,并且要添加的元素与当前数组元素相等
                // 说明快照与当前current之间数组发生了修改,并且设置了数组某一元素为e,已经存在,直接返回
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            // 在当前数组中查找元素
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        // 进行后续的添加操作
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

这个方法我们来看下流程:

  • 首先还是相同的操作,获取锁,获取当前数组,获取数组长度;
  • 然后判断原先的快照数组和当前的数组是否相等,不相等则说明数组发生了修改;
  • 这个时候取数组长度较小的值,进行遍历操作;
  • 如果当前数组元素与快照数组元素不相等,并且要添加的元素与当前数组元素相等,说明快照与当前数组current之间,数组发生了修改,并且设置了数组某一元素为e,说明已经存在,直接返回;
  • 如果上述条件没有发生,则从下标common开始再次进行查找操作;
  • 如果查找不到,进行后续的添加操作;
4.3 indexof方法
public int indexOf(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length);
}

private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
    // 如果要查找的元素是null
    if (o == null) {
        for (int i = index; i < fence; i++)
            // 如果有值是null的直接返回
            if (elements[i] == null)
                return i;
    } else {
        // 遍历,根据equals方法进行判断
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}

indexof方法表示从前往后查找该元素所在的第一个下标值,而与之相对应的则是lastIndexOf方法,这里也不多说了。

4.4 set方法

同样,set方法也很简单,我们来看一下:

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        // 先获取index处的元素值
        E oldValue = get(elements, index);
        // 比较下旧的值和新的值是否相同
        if (oldValue != element) {
            // 不相同,复制一个新的数组,并设置对应index处的值为新的值
            // 再更新数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
4.5 Array.copyof方法

CopyOnWriteArrayList这个类的操作基本都是通过Array.copyof方法来实现的,我们顺便来看下这个方法的源码:

public static  T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static  T[] copyOf(U[] original, int newLength, Class newType) {
    @SuppressWarnings("unchecked")
    // TODO,这里没看太懂,等以后来补充
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

可以看到,该方法最终是通过系统类System的底层方法arraycopy来实现的。由于方法内部的操作都差不多,所以看了这几个方法的源码后,其他方法的源码就不用看了,比如说remove方法,和重载的add方法操作是类似的。

三、CopyOnWriteArraySet

  最后,再来简单看下CopyOnWriteArraySet这个类,这个类就更简单了,底层封装了一个CopyOnWriteArrayList对象,所有对该Set的操作,实际上都是通过对CopyOnWriteArrayList来操作的。同样,该类和CopyOnWriteArrayList有相同的一些特性,比如适用于当查询操作远远多于修改操作的情况;

  该类继承了AbstractSet类,从而可以最大程度的减少Set接口的实现;而CopyOnWriteArraySet实现元素的不重复,则是借助于CopyOnWriteArrayList中的addIfAbsentaddAllAbsent方法来实现的。比如:

public class CopyOnWriteArraySet extends AbstractSet
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList al;

    public CopyOnWriteArraySet(Collection c) {
        if (c.getClass() == CopyOnWriteArraySet.class) {
            @SuppressWarnings("unchecked") CopyOnWriteArraySet cc =
                (CopyOnWriteArraySet)c;
            al = new CopyOnWriteArrayList(cc.al);
        }
        else {
            al = new CopyOnWriteArrayList();
            al.addAllAbsent(c);
        }
    }

    public boolean add(E e) {
        return al.addIfAbsent(e);
    }

}

四、总结

  CopyOnWriteArrayList的源码其实特别简单,该类的实现就是围绕着复制来实现的,也就是围绕着Arrays.copyOf方法来实现的,这个方法了解了,该类的核心思想也就理解了。而CopyOnWriteArraySet则更简单了,只是封装了一个CopyOnWriteArrayList,然后对这个List进行操作即可。另外,Arrays.copyof方法还有点没看明白,等哪天参考下某位大神的博客后,再来补充下。

本文参考自:
《Java并发编程实战》
【JUC】JDK1.8源码分析之CopyOnWriteArrayList(六)

你可能感兴趣的:(Java1.8-CopyOnWriteArrayList源码解析(三))