JUC1.8-CopyOnWriteArrayList源码分析

前言

CopyOnWriteArrayList 原理:
先通过名字定义来看,“在写时复制的列表” 其原理也是如名字含义显而易见。 先看几个着重点:
1、线程安全
2、适合多读少写场景
3、弱一致性
4、迭代器不支持可变操作【add,set,remove】
大家先把这4点留个印象在脑海里,带着这些点,咱们通过源码跟踪【add,set,indexOf ,remove】进行逐一证实上述观点。

1、数据结构

/** 互斥锁 */
final transient ReentrantLock lock = new ReentrantLock();

/** 数组*/
private transient volatile Object[] array;

/**
 * 初始化一个数组长度为0的列表
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

/**
 * 通过集合方式初始化
 */
public CopyOnWriteArrayList(Collection 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);
}

/**
 * 通过固定数组初始化
 */
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

2、看看是怎么保证读 写 删的线程安全?

1. 写入方法解析【set、add】

  • set(int index, E element) ----此方法是将指定下标的值,替换成指定的val。 如果当前下标的值为空,那么不进行任何操作。
public E set(int index, E element) {
        //获取锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //get 当前数据
            Object[] elements = getArray();
            //获取指定下标对应的数组值
            E oldValue = get(elements, index);
            //如上述获取不为空.
            if (oldValue != element) {
                //获取当前数组的总长度
                int len = elements.length;
                //注意这里是并发的安全的要点
                //会将原来old的数组复制出来,在新的数组上,进行oldVal的替换
                Object[] newElements = Arrays.copyOf(elements, len);
                //基于新数组进行值替换
                newElements[index] = element;
                //最后数组的转移至新的数组
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                //如果指定的index没有数据,那么不做任何处理.
                setArray(elements);
            }
            //返回老的数据
            return oldValue;
        } finally {
            //解锁
            lock.unlock();
        }
    }
  • boolean add(E e)----此方法是添加数据至数组的尾部。
public boolean add(E e) {
        //加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //获取当前数组
            Object[] elements = getArray();
            //数组长度
            int len = elements.length;
            //基于old数组长度上 + 1 复制一个新的数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            /**
             * 在新数组的尾部赋予新值
             * 在这会有不会有人看迷糊?  为啥上面用len作为新值得下标?? 简单提下:
             *
             * 因为数组存值是从0开始,那么也就意味着总长度,
             * 始终是数组的最后一个值得下标:总长度-1, 所以上面进行了len + 1 正好就补充进来了.
             *
             * 例如 先数组有 1 2 3 4值. len = 4
             * 那么各个值对应的下标 : 1=0 2=1 3=2 4=3
             * 最后的4 的下标是 len - 1, 这时我在add 5 进来,
             * 此时需要把len + 1增大数组的容量, 那么Newlen = 5了, Oldlen = 4也就是5的在数组对应的位置了.
             *
             */
            newElements[len] = e;
            //在转移新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
  • void add(int index, E element)----此方法是在指定位置进行插入操作,然后将指定位置的数据均往后移一位
public void add(int index, E element) {
        //加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //获取当前数组
            Object[] elements = getArray();
            //获取数组长度
            int len = elements.length;
            //如果指定的下标,大于数组长度,或者下标<0 那么抛出越界异常
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            //预先声明新数组
            Object[] newElements;
            /**
             * 划重点:  为什么常说list不适合频繁插入和删除操作?
             *
             * numMoved = 数组长度 - 指定下标 = 移动的长度
             *
             * 1. 如果numMoved  = 0 那么说明指定的index 与 数组长度是一致的,其实就是说在数组的尾部添加值.就是add(E e)的逻辑
             * 在原数组的长度上 + 1进行对原数组的copy ,并把值赋值到尾部
             *
             * 2. 如果numMoved > 0,那么先声明新数组,在原数组基础扩大一位,
             * 2.1 从原数组的首位开始,copy index数量的数据到新数组中,从首位开始赋值
             * 2.2 从原数组的第index位置开始,copy 前面计算的移动长度numMoved 个数据到新数组中,从index + 1的位置开始赋值
             * 2.3 因此新数组的第index的下标就空出了,在将val值赋值
             */
            int numMoved = len - index;
            if (numMoved == 0)//扩大一位,赋值
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                //声明len + 1的数组
                newElements = new Object[len + 1];
                //从原数组的 第0下标开始 copy index个数据 之新数组,从0下标开始
                System.arraycopy(elements, 0, newElements, 0, index);
                //从原数组的 第index下标开始,copy numMoved个数据 之新数组,从index + 1下标开始
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //将index 赋值val
            newElements[index] = element;
            setArray(newElements);
        } finally {
            //解锁
            lock.unlock();
        }
    }

以上介绍了三种写入数据的方法,那么由此可以看出不论是set的替换数据,以及是在数据尾部或者指定位置插入数据,均有一个Arrays.copyOf(elements, len) 或者是System.arraycopy(elements, 0, newElements, 0, index)。 那么这个是干啥的呢?

这就是对应 写时复制的思想, 在对数组进行可变操作时,都先在原数组的基础上copy出来,在新的数组上,进行相对应的可变操作,操作完毕后,在将新数组指向到全局volatile Object[] array。
so 所以每一次的可变操作,都得进行一个新数组的生成,如果是在指定的位置操作,那么还得将指定位置后面的所有数据,一一往后一位移动。 这个性能就不言而喻了。 所以这就是为什么CopyOnWriteArrayList只适合多读少写的场景

那么写入 是咋保证线程安全的呢? ----------可以看到容器这块君都使用互斥锁ReentrantLock#lock(); 方式帮我们进行控制,因此在写入时不必担心线程安全;

2. 读方法解析【get】:

杂一看我去,为啥get()方法实现如此之简单,用什么保证读安全的? 其实在我们了解了上述CopyOnWriteArrayList写入的机制后,再来看读方法,你是不是已经有眉目? 先来看下简单的源码:

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

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
    
    /**
     *  获取指定val的下标。

     index
     */
   public int indexOf(E e, int index) {
        Object[] elements = getArray();
        return indexOf(e, elements, index, elements.length);
    }

由源码的知道,在读取数据时,直接是通过后获取数组的下标进行读取, 那么保证读取安全的第一个要点:

1 、 private transient volatile Object[] array; 使用volatile修饰,保证可见性,有序性
2、其次是在进行可变操作时, 都先预备新的数组,操作完成后在进行主内存中新老数组替换,通过volatile保证被修改后,所有线程看到数组是最新的。

那么也由此可见 该容器是弱一致性 为什么这说呢?

可能在某一时刻,在指定位置插入时, 新的数组已经生成,但还在进行老数组的copy中,此时有线程来读取数据了,那么这时读取的数组 与 最终的数组就不是一致的。

3. 删除方法解析【remove】

remove方法也是属于可变操作中一种,因此也是采用写时复制的思路。

E remove(int index) 源码如下:

public E remove(int index) {
    //加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //获取当前数组
        Object[] elements = getArray();
        //过去当前数组长度
        int len = elements.length;
        //获取指定下标的val值
        E oldValue = get(elements, index);

        /**
         * 这里重点分析下numMoved三种情况
         *
         * 1 numMoved = 0  说明移除的最后一个数据. 所以直接把 len-1后的老数组copy过去即可.
         *
         * 2 numMoved < 0  说明已经越界了,数组里面并没有,指定index.   在E oldValue = get(elements, index);就抛出越界异常了.
         *
         * 3 numMoved > 0  说明需要移除的是非最后一位,那么就要为找index这个位置进行0-index-1的转移,以及index-1 - 尾部的数据转移
         *
         */
        int numMoved = len - index - 1;
        if (numMoved == 0)
            //移除最后一位,直接在原数组上 进行len-1操作即可.
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            //移除非最后一位,那么先声明一个len -1的新数组
            Object[] newElements = new Object[len - 1];
            //从old数组的第0位开始,转移index个数据到新数组中,也是从0开始
            System.arraycopy(elements, 0, newElements, 0, index);
            //从old数组的第index+1位开始,转移numMoved个数据到新数组中,也是从index开始
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            //最后指向新的数组
            setArray(newElements);
        }
        //返回移除掉的val值
        return oldValue;
    } finally {
        //解锁
        lock.unlock();
    }
}

所以可以看出根据index移除数据直接使用ReentrantLock方式进行线程安全保护。这里保护有啥作用的? 当持有此锁时,其他可变操作就需要等待锁释放,那么当前数组是不会可变了,因此直接找到对应index移除即可;

总结

经过上述方法解析,相信大家已经有锁了解了。 但是COWList也不是完美的,因为它的迭代器是无法支持可变操作【set,add,remove】。 但也没关系,是可以支持在for循环中使用,均是采用写时复制的思路, 所以由衷的建议大家,不要轻易操作可变操作。 如果数据非常大的情况下,进行可变操作,那么内存就是立马飙升,以至于频繁FullGc,严重可能导致OOM。 这样就得不偿失了。

其次读写是弱一致性,只能保证最终一致。 那么对于实时要求较高的场景也是不适用的。

可见如此作者设计该容器时,就是认为读要大于写的这么一种思想。 多用于固定数据下,并发读的场景。

你可能感兴趣的:(JUC,JUC)