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

一、概述

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

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

二、CopyOnWriteArrayList

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

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

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

public CopyOnWriteArrayList(Collection<? extends E> 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<E> implements ListIterator<E> {
    /** 基础数组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<? super E> 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> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> 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<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList<E> al;

    public CopyOnWriteArraySet(Collection<? extends E> c) {
        if (c.getClass() == CopyOnWriteArraySet.class) {
            @SuppressWarnings("unchecked") CopyOnWriteArraySet<E> cc =
                (CopyOnWriteArraySet<E>)c;
            al = new CopyOnWriteArrayList<E>(cc.al);
        }
        else {
            al = new CopyOnWriteArrayList<E>();
            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(六)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,085评论 1 32
  • 不足的地方请大家多多指正,如有其它没有想到的常问面试题请大家多多评论,一起成长,感谢!~ String可以被继承吗...
    启示录是真的阅读 2,920评论 3 3
  • Java SE 基础: 封装、继承、多态 封装: 概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽...
    Jayden_Cao阅读 2,099评论 0 8
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 5,085评论 0 23
  • 我就是个人渣!
    我要开始写字了阅读 173评论 0 0