CopyOnWriteArrayList源码分析

       如何线程安全的操作一个数组对象,类似于前面分析的ArrayBlockingQueue以及Vector,实际上都是使用加锁来实现,只不过第一个底层是使用ReentrantLock,第二个底层采用synchronized关键字来实现。对于任意一个线程访问数组,都会阻塞其他线程。但是实际上对于一个数组来说,当我们并发的去读的时候是不会出现并发问题的,因此如果可以在并发读的情况下不加锁,而在修改的时候加锁,理论上是可以增加吞吐量的。

      而实际上,并发读不加锁,修改时阻塞其他修改线程,这里实际上使用一个新的数组来修改,可以认为这是一个新的副本上的操作,因此不会影响其他读线程的读操作,当修改线程完成修改之后会整体替换原来的数组。这就是整个CopyOnWriteArrayList的基本原理。

      再来说一下为什么这样快一些,在普通的写线程加锁阻塞这一过程中,所有的读线程都是会阻塞的,而在CopyOnWriteArrayList中,当有线程修改数组,直接在副本上操作,完全不影响原来的读线程,这样就会极大的增加并发量。这里相当于以空间换时间


CopyOnWriteArrayList重要参数和构造函数

       底层是一个非公平锁+一个数组(使用了volatile关键字,修改了立刻可见)。构造函数可以不传参数,此时生成一个空的数组,或者是可以传入一个容器类。通过getArray和setArray来分别用来获得底层数组和替换底层数组。

 /** 非公平锁 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** 全局可见数组. */
    private transient volatile 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;
    }

    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    /**
     * Creates a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection of initially held elements
     * @throws NullPointerException if the specified collection is null
     */
    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);
    }

读操作

     读操作很简单,就是一个数组里根据下标值获取元素,这里不会有任何阻塞操作。

 // Positional Access Operations

    @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);
    }

写操作

      以set方法和add方法为例,我们可以看出这两个操作都在开始的时候加锁,也就是说如果这个时候有其他线程想要修改数组的时候都会被阻塞。这里的具体过程是获取原来的旧数组,然后根据旧数组copy出一个新的数组,在上面进行修改和添加的操作。这样的话是不影响读操作的,当线程修改完成数组之后,将原来的数组引用指向新数组就可以了。

 /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 添加操作更为简单,首先加锁,生成一个新数组,然后在后面添加一个元素。
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    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();
        }
    }

总结

    感觉CopyOnWriteArrayList中的方法都是比较简单的,重要的可能是其中有关读写分离的思想,对于读和写来说,本质上操作的是不一样的容器。

     CopyOnWriteArrayList不会在写的时候阻塞读线程,这样就可以大大的增加并发量。

     如果写操作比较多的时候,会产生大量的冗余数组,因此实际上CopyOnWriteArrayList更适合读多写少的情况。

    CopyOnWriteArrayList的读操作,不能保证数据是实时一致的,比如在写的时候读线程读的仍旧是旧的数组。因此一致性很强的时候不要使用这个类。


参考资料

http://ifeve.com/java-copy-on-write/

https://www.jianshu.com/p/5f570d2f81a2

https://blog.csdn.net/hua631150873/article/details/51306021

https://blog.csdn.net/linsongbin1/article/details/54581787


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值