现在的位置: 首页 > 综合 > 正文

Java concurrent Framework并发容器之CopyOnWriteArrayList(1.6)源码分析

2019年09月28日 ⁄ 综合 ⁄ 共 8708字 ⁄ 字号 评论关闭

简介

CopyOnWriteArrayList是jdk concurrent包中提供的一个非阻塞型的,线程安全的List实现。CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的ArrayList。

CopyOnWriteArrayList在进行数据修改时,会对数据进行锁定,每次修改时,先拷贝整个数组,然后修改其中的一些元素,完成上述操作后,用一个原子操作替换整个数组的指针。
对CopyOnWriteArrayList进行读取时,不进行数据锁定,直接返回需要查询的数据,如果需要返回整个数组,那么会将整个数组拷贝一份,再返回,保证内部array在任何情况下都是只读的。

“CopyOnWrite”简介

在学习Linux操作系统进程/线程的时候就听到过这个概念,直接翻译过来叫“写时拷贝”,说的是当需要创建一个新线程,做fork()调用,而这时并未真正意义上开辟一块新的线程内存资源,仅当线程有“写操作”时,才会拷贝父线程的数据作为新线程的初始内容。

而在JavaSE5的并发包中,CopyOnWriteArrayList的实现也是利用了类似的方法。当数组内容有所变化时,拷贝一份新的出来,并把对象引用赋值给List的属性,旧数组的访问也依然有效,不会受到新的操作干扰。

CopyOnWriteArrayList适合什么场景?

容器对象的改动有很多种,对于CopyOnWriteArrayList每发生一次改变,CopyOnWriteArrayList就会复制一份数据,显然这个也是需要成本的。所以,CopyOnWriteArrayList其实是更适用于读操作远远多于写操作的场景

CopyOnWriteArrayList

public class CopyOnWriteArrayList<E>  implements List<E>, RandomAccess, Cloneable, java.io.Serializable

但即使有了CopyOnWrite,在写操作时,创建新数组对象,并将其引用赋值时,仍然是会受到多线程的影响,这个还是要通过锁来保护。

transientfinal
ReentrantLock lock =new
ReentrantLock();

这就是CopyOnWriteArrayList类中的锁属性。

CopyOnWriteArrayList中,包含一个array数组对象,这个对象,只能由getArray()和setArray()两个方法访问,源码如下:

    /** The array, accessed only via getArray/setArray. */
    private volatile transient Object[] array;

    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

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

CopyOnWriteArrayList构造函数

CopyOnWriteArrayList提供了三个构造函数,分别为
  • CopyOnWriteArrayList():构造一个内容为空的对象
  • CopyOnWriteArrayList(Collection<? extends E> c):根据传入参数中的内容,构造一个新的对象
  • CopyOnWriteArrayList(E[] toCopyIn):根据传入数组中的内容,构造一个新的对象

      上述三个方法的源码如下:
1    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
  创建一个大小为0的数组

2    public CopyOnWriteArrayList(Collection<? extends E> c) {
    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);
    }
    将传入的容器转换为数组,如果传入的元素不能直接转换为数组类型,创建一个数组然后将复制到新数组,然后通过一个原子操作设置给array属性。
  根据传入参数c的长度,构造一个同样长度的Object[]对象,并且将c的内容,依次填入此Object[]对象中
  (1). 这里的构造函数,未显式判断c是否为null,实际上如果c为null,会抛出空指针异常
  (2). 这里对于c中内容的复制,是浅拷贝而非深拷贝
深拷贝(深复制)和浅拷贝(浅复制)概念解释:

深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问题,但是我们在这幸好用的是Java。虽然java自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑。

浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:对象A1中包含对B1的引用,B1中包含对C1的引用。浅拷贝A1得到A2,A2 中依然包含对B1的引用,B1中依然包含对C1的引用。深拷贝则是对浅拷贝的递归,深拷贝A1得到A2,A2中包含对B2(B1的copy)的引用,B2 中包含对C2(C1的copy)的引用。

若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝
   
3     public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
根据传入参数的长度,构造出一个同样长度,内容一致的数组对象,封装在CopyOnWriteArrayList中

CopyOnWriteArrayList读操作

读的相关操作

对于读操作来讲,是不需要加锁的,直接读取引用对应的对象即可。因为即使数组有所改动,也是开辟新的数组并在新的数组中做修改,旧数组对象是始终可用的。

public E get(int index): 根据索引获取对应的元素,读取不需要加锁

public Iterator<E> iterator() 调用iterator方法后创建一个新的COWIterator对象实例,并保存了一个当前数组的快照,在调用next遍历时则仅仅对快照数组进行遍历,因此遍历CopyOnWriteArrayList时不会抛出ConcurrentModificatiedException。

public ListIterator<E> listIterator()

public ListIterator<E> listIterator(final int index)

1  get操作
   public E get(int index) {
        return (E)(getArray()[index]);
    }
该操作简单,直接获取当前数组对应位置的元素,这种方法是没有加锁保护的,因此可能会出现读到脏数据的现象。但相对而言,性能会非常高,对于写少读多且脏数据影响不大的场景而言,CopyOnWriteArrayList是不错的选择。

2   iterator()
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
调用iterator方法后创建一个新的COWIterator对象实例,并保存了一个当前数组的快照,在调用next遍历时则仅仅对快照数组进行遍历,因此遍历CopyOnWriteArrayList时不会抛出ConcurrentModificatiedException。

迭代器Iterator的使用。CopyOnWriteArrayList内部给出了COWIterator类的实现,也非常简单,只有数组snapshot属性和索引cursor属性。snapshot的名字定义得非常好,意为“快照”。实际上CopyOnWriteArrayList的迭代器对象就是在生成时的那一个快照,后续的迭代读操作都是在这个快照基础上进行的。

COWIterator意思CopyOnWriteIterator

    private static class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the 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;
        }

        public E next() {
        if (! hasNext())
                throw new NoSuchElementException();
        return (E) snapshot[cursor++];
        }

        public E previous() {
        if (! hasPrevious())
                throw new NoSuchElementException();
        return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

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

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; <tt>remove</tt>
         *         is not supported by this iterator.
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; <tt>set</tt>
         *         is not supported by this iterator.
         */
        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; <tt>add</tt>
         *         is not supported by this iterator.
         */
        public void add(E e) {
            throw new UnsupportedOperationException();
        }
    }

CopyOnWriteArrayList写操作

CopyOnWriteArrayList中是有ReentrantLock类的锁对象属性的。在修改CopyOnWriteArrayList类的对象时,是需要考虑多个线程访问array对象的。那么,锁的使用也就是必须的了。
在CopyOnWriteArrayList中,CopyOnWrite的过程通常是这样进行的。先在原数组对象属性的基础上copy出一个新的数组,其它改动操作都在新的数组对象上完成,当所有修改操作完成后,将新数组对象的引用交给CopyOnWriteArrayList类的array属性对象。整个过程在锁的保护下进行。

1 新增
    public boolean add(E e) 加锁操作,将元素e写入列表尾部
    public void add(int index, E element) 将元素插入到索引位置index
    public boolean addAll(int index, Collection<? extends E> c) 将集合c插入到索引index位置
    public boolean addAll(Collection<? extends E> c) 将集合c插入到列表尾部
    public boolean addIfAbsent(E e) 如果元素e不在列表中则将元素插入到列表中
    public int addAllAbsent(Collection<? extends E> c) 如果集合c不在列表中,则将集合c插入到列表中
2 修改
    public E set(int index, E element) 用元素element替换index位置的元素
3 删除
    public E remove(int index) 删除index位置的元素
    public boolean remove(Object o) 删除元素o
    public boolean removeAll(Collection<?> c) 删除集合c
  

1.1 add(E e)
    public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
      try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
      } finally {
        lock.unlock();
      }
    }
分析:add方法没有加关键字synchronized,而是通过ReentrantLock来保证线程安全,此处和ArrayList的不同是每次都会创建一个新的Object数组,此数组的大小为当前数组大小加1,将之前数组中的内容复制到新的数组中,并将新增加的对象放入数组末尾,最后做引用切换将新创建的数组对象赋值给全局的数组对象。

     public boolean remove(Object o) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (len != 0) {
        // Copy while searching for element to remove
        // This wins in the normal case of element being present
        int newlen = len - 1;
        Object[] newElements = new Object[newlen];

        for (int i = 0; i < newlen; ++i) {
            if (eq(o, elements[i])) {
            // found one;  copy remaining and exit
            for (int k = i + 1; k < len; ++k)
                newElements[k-1] = elements[k];
            setArray(newElements);
            return true;
            } else
            newElements[i] = elements[i];
        }
        // special handling for last cell
        if (eq(o, elements[newlen])) {
            setArray(newElements);
            return true;
        }
        }
        return false;
    } finally {
        lock.unlock();
    }
    }
分析:该方法也是通过ReentrantLock来保证线程安全的,首先创建一个比当前数组小1的数组,遍历数组,如找到equals或均为null的元素,则将之后的元素全部赋值给新的数组,并做引用切换,返回true,如未找到,则将当前的元素赋值给新的数组对象,最后特殊处理数组中的最后一个元素,如最后一个元素等于要删除的元素,即将当前数组对象赋值为新创建的数组对象,完成删除动作,如最后一个元素也不等于要删除的元素,那么返回false. 此方法复制过程没有调用System的arrayCopy来完成,

CopyOnWriteArrayList其他操作

public boolean isEmpty()
public boolean contains(Object o)
public boolean containsAll(Collection<?> c)
public int size()
public boolean equals(Object o)
public void clear() 清除数组

CopyOnWriteArraySet简介

在java.util.concurrent包中,除了CopyOnWriteArrayList类,也还有这样一个类CopyOnWriteArraySet。CopyOnWriteArraySet的实现是完全基于CopyOnWriteArrayList的。
public class CopyOnWriteArraySet<E> extends AbstractSet<E>  implements java.io.Serializable

构造函数:    private final CopyOnWriteArrayList<E> al;

    /**
     * Creates an empty set.
     */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

可以看出CopyOnWriteArraySet基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法。addIfAbsent方法同样采用锁保护,并且在一个新的大小+1的Object数组。遍历当前Object[]数组,如Object[]数组中已有了当前元素,则直接返回,如没有则放入Object[]数组的尾部,并返回。从分析可以看出CopyOnWriteArraySet在add时每次都要进行数组的遍历,因此其性能会略低于CopyOnWriteArrayList。

更详细的大家可以参看JDK,欢迎补充。

参考

JDK1.6 CopyOnWriteArrayList源码

抱歉!评论已关闭.