程序员

jdk源码CopyOnWriteArrayList分析基于JDK

2018-09-16  本文已影响12人  超级小学生1

简介

在通过介绍 Vector 时提到了 CopyOnWriteArrayList 类。它也属于Collection 家族下的一个线程安全的一个集合,而且同样基于对象数组实现。所以,目前基于对象数组实现的目前接触到了 ArraylistVector ,和当前介绍的CopyOnWriteArrayList。而且,三者也可以拿来对比一下各自的应用场景。

1.首先,CopyOnWriteArrayList基于对象数组,但是并不意味着增删比较快(重点),其应用场景在于读多写少情况(埋个伏笔)。

2.CopyOnWriteArrayList实现了 RandmoAccess接口。(同之前一样,就是为了实现快速访问,在ArrayList和vector中已经提出过)。

3.CopyOnWriteArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。

4.CopyOnWriteArrayList 实现java.io.Serializable接口,支持序列化。

5.CopyOnWriteArrayList,是线程安全的。注意 这里单独是写入操作是线程安全的。

通过上面的几点(如果你看了前三节总结的知识点),首先可以总结到 Collection 接口下,很多的工具类提供了很多相似的功能,绝大部分实现了,Cloneable接口,java.io.Serializable接口,RandomAccess接口。这里体现出了一点,面向接口的编程,更便于扩展。不同的工具类只需扩展对应的接口,然后接下来实现关于自己的特性即可。这点非常的重要,这也是延伸出来可以学习到更重要的知识。

继承实现体系:

BE70E0D5-5AC6-4FF0-AB80-4396BD8AFAAE.png

构造函数

共有三个构造函数,分别为:

1.无参数

/**
 * 创建一个空的列表
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

2.以一个 Collection 为参数构造一个新的列表。

 /**
 * 创建包含指定元素的列表集合,按照集合返回的顺序的迭代器.
 *
 * @param c the collection of initially held elements
 * @throws NullPointerException if the specified collection       * is null
 */
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    //这里是判断是否为同一个类型。
    //c.getClass()返回包路径,和当前class名称。
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
          //转为array,然后调用Arrays.copyOf(),来构造新的list
        elements = c.toArray();
        // defend against c.toArray (incorrectly) not returning Object[]
        // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    //这里将返回的新的Array复制给全局变量array
    setArray(elements);
}

 **
 * Sets the array.
 */
final void setArray(Object[] a) {
    array = a;
}

/** The array, accessed only via getArray/setArray. */
//volatile关键字,代表此array修改之后立刻对其他线程可见。
//但是要注意,在CopyOnWriteArrayList内,对array的操作都是通过setArray方法。
//因为有一个统一设置array的入口,当array改变后,可以直接对其它线程可见。
private transient volatile Object[] array;

3. 以数组作为参数,造一个新的列表。

  /**
 * 通过一个数组来穿件集合
 *
 * @param toCopyIn the array (a copy of this array is used as the
 *        internal array)
 * @throws NullPointerException if the specified array is null
 */
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

常用方法

1.add()方法

 /**
 * 添加一个节点在集合末尾
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
      //synchronized说明,修改集合时,只有一个线程可以操作当前集合。
    synchronized (lock) {
          //获得当前的集合
        Object[] elements = getArray();
        int len = elements.length;
        //复制原来的集合
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //设置新的节点内容,在集合末尾
        newElements[len] = e;
        //将array引用指向到新数组
        setArray(newElements);
        return true;
    }
}



 /**
 * 添加元素到指定索引
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
      //同样线程安全
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException(outOfBounds(index, len));
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
                //numMoved为0说明index为当前链表末尾
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            //创建新的数组,长度为原来素组长度+1
            newElements = new Object[len + 1];
            //复制,index之前数组元素
            System.arraycopy(elements, 0, newElements, 0, index);
            //复制index之后元素
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        //设置index处元素
        newElements[index] = element;
        // 将array引用指向到新数组
        setArray(newElements);
    }
}   

注意

通过add方法应该意识到,每当有元素添加的操作时。总是先把当前的集合复制一份,然后返回并且添加新的元素到当前集合,然后才调用setArray方法,使新的集合生效。这也就意味着,CopyOnWriteArrayList的增加元素操作极为耗资源。同比,ArrayList他是提前把数组的长度做扩容,然后将新的元素添加到数组的末尾,但是CopyOnWriteArrayList不能这么做。原因在于,在每次的数据改动时,因为为了避免影响读操作,都需要copy一个原有集合的副本,当操作完毕将引用指向新的集合对象。而并不是在原有的对象上做数据的改动处理,因此扩容也就没有意义。

remove()

/**
 * 删除集合内指定索引处的元素
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        //需要删除的元素
        E oldValue = elementAt(elements, index);
        //元素需要删除的索引信息
        int numMoved = len - index - 1;
        if (numMoved == 0)
               //numMoved为0,删除最后一个节点
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            //创建新的数组对象
            Object[] newElements = new Object[len - 1];
            //复制index之前的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            //将index后的元素,从index开始替换。
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            //将array引用指向到新数组
            setArray(newElements);
        }
        //返回删除的元素
        return oldValue;
    }
}

get()

/**
 * 读操作。
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    return elementAt(getArray(), index);
}

注意:唯一你需要注意到的就是这里没有添加关于线程安全的 synchronized 关键字。

总结

1.CopyOnWriteArrayList 数组有改动,都需要耗费过多的资源用来拷贝数组的副本。因此,读多写少的场景最适合应用,这个就要看具体的业务了。

2.CopyOnWriteArrayList 添加或者删除操作,并不能保证达到实时读。因为数据的引用没有指向新的对象之前,读的仍是旧的数据。(setArray方法调用之前读的仍然是旧数据)。

3.这里更好的体现了一个思想时,读写分离。在多线程应用下,达到线程安全。并且保证数据的一致性。

4.多线程情况下,采用对象副本的方式解决了线程并发冲突问题。

上一篇下一篇

猜你喜欢

热点阅读