CopyOnWriteArrayList类

CopyOnWriteArrayList是一个线程安全的ArrayList,对其修改操作都是在底层的一个复制的数组上进行的,也就是使用了写时复制策略。

 

 

 一、变量与构造方法

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();//独占锁,保证同一时刻只有一个线程对array修改

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;//存放对象的数组

    public CopyOnWriteArrayList() {//无参实例化 array = new Object[0];
        setArray(new Object[0]);
    }

    /**
     * Collection接口实例创建实例化 将Collection的array复制给CopyOnWriteArrayList的array*/
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            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*/
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

二、写时复制策略:增删改时复制数组,修改后替换原数组。

1.boolean add(E e)方法

    public boolean add(E e) {
        //获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //复制array到新数组,添加元素到新数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            //新数组替换原数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

2.E remove(int index)

    public E remove(int index) {
     //获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            //删除最后一个元素
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
          //创建新数组,分两次复制删除后的剩余的元素到新数组
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                //新数组替换原数组
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

3.E set(int index, E element):

    public E set(int index, E element) {
        //获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            //修改元素与原元素不同,创建新数组,修改对应下标的元素
            if (oldValue != element) {
                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 不是完全没有操作,需要确保volatile的写语义
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

三、弱一致性问题,读操作时不能满足实时性要求

弱一致性问题是由写时复制策略产生的,读写操作并发时,读操作时数组的版本可能是写操作版本的原数组,不是替换的新数组。不能保证实时性要求。

1.E get(int index):未加锁,弱一致性问题:remove()、set()的同时执行get()方法,可能会导致获取已删除、修改前的元素。

    public E get(int index) {
        return get(getArray(), index);
    }

    private E get(Object[] a, int index) {
        return (E) a[index];
    }

2.迭代器iterator的弱一致性fast-safe机制,返回迭代器后,遍历的是返回迭代器时数组的一个快照。其他线程对list的增删改对迭代器是不可见的。

快照版本,指针指向的是增删改前的数组,而不是增删改后的新数组,操作的是两个不同的数组

为什么写操作加锁了已经能保证线程安全了,还要复制数组?这里可能就是答案吧,读操作有get和iterator,复制数组是为了实现fast-safe机制,避免fast-fail。

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

    static final 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 class COWTest {

    private static volatile CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        cowList.add("hello");
        cowList.add("world");
        cowList.add("skip1");
        cowList.add("skip2");
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                cowList.set(0,"hello ");
                cowList.remove(3);
                cowList.remove(2);
            }
        });

        Iterator<String> iterator = cowList.iterator();

        threadOne.start();
        threadOne.join();

        while (iterator.hasNext()){
            System.out.println(iterator.next());//hello\nworld\nskip1\nskip2\n
        }

        System.out.println(cowList);//[hello , world]
    }
}

总结:

1.写时复制策略,写时加锁了已经可以保证线程安全了,复制是为了避免fast-fail机制,实现fast-safe,每次增删改都会复制生成一个新数组,当数组太大时,每次写操作代价太大,性能很低,另外JVM规定大对象会直接进入老年代,可能导致full gc

2.弱一致性,虽然能做到最终一致性,但是不能保证实时性需求

3.将读写分开处理,适合读多写少的场景。

4.扩容与ArrayList不同,add时+1,remove时-1

4.增删改共用同一个独占锁,所以操作互斥。

参考自《java并发编程之美》

posted on 2020-01-07 09:17  FFStayF  阅读(288)  评论(0编辑  收藏  举报