源码分析-CopyOnWriteArrayList

Posted 千念飞羽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码分析-CopyOnWriteArrayList相关的知识,希望对你有一定的参考价值。

概述

doc文档

一个线程安全的ArrayList对于所有的可变操作都使用一个基于数组的新拷贝实现。
通常的情况下这样做的代价非常大,但是当遍历操作远远大于修改操作的时候这样做更有效率,或者当你不希望使用同步的方式遍历操作同时又希望可以排除并发干扰的时候也很有效。快照类型的迭代器使用给指向当迭代器创建的时候当前数组状态的引用。这个数组在整个迭代器的生命周期中都不变,所以同步干扰不会出现,而且也不会抛出ConcurrentModificationException异常。这个迭代器不会反映出在该迭代器创建之后的其迭代对象的变化。也不允许通过迭代器修改元素,修改操作会返回UnsupportedOperationException。
允许null元素。

首先静态域:

    private static final sun.misc.Unsafe UNSAFE;
    private static final long lockOffset;
    private static final long serialVersionUID = 8673264195747942595L;
    transient final ReentrantLock lock = new ReentrantLock();
    private volatile transient Object[] array;

注意这里的lock是transient的。所以这里不会做序列化。所以这里需要UNSAFE和lockOffset在序列化和克隆之后去创建一个新的lock。然后用UNSAFE.putObjectVolatile来设置。

这里来看一下resetLock()

    private void resetLock() 
        UNSAFE.putObjectVolatile(this, lockOffset, new ReentrantLock());
    

这个方法是可以根据指定的偏移量来设置一个类的域。这个方法可以任意的修改类的域变量,通常来说建议这个域是volatile。这里lock虽然不是volatile但是是final所以也提供可见性。道理是一样的。这个方法只用在两个地方,一个是clone,一个是deserialization。这里实现上看比较简单,先看下是怎么实现的然后去分析下为什么这样做。

    public Object clone() 
        try 
            CopyOnWriteArrayList c = (CopyOnWriteArrayList)(super.clone());
            c.resetLock();
            return c;
         catch (CloneNotSupportedException e) 
            // this shouldn't happen, since we are Cloneable
            throw new InternalError();
        
    

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException 

        s.defaultReadObject();

        // bind to new lock
        resetLock();

        // Read in array length and allocate array
        int len = s.readInt();
        Object[] elements = new Object[len];

        // Read in all elements in the proper order.
        for (int i = 0; i < len; i++)
            elements[i] = s.readObject();
        setArray(elements);
    

从实现上看这两个方法都是很正常的。但是这里有一个问题为什么要用final和transient组合,以及产生的影响。这种组合通常来说是比较少见的,我搜罗一下网上的资料。这里总结一下
final transient的域如何设置值
final transient的域为null
final transient的作为锁对象
类似的问题小结

  • 首先一个问题 final和transient的组合通常用在什么地方

通常来说final类型就是用设置之后就不会改变的情况。常规情况下,有两种情况去赋值,一种是直接在域中指定,另一种是在构造器中指定。transient用于不适合序列化的数据,比如说对于密码,或者缓存之类。这样值在序列化的过程中就会被忽略了。如果重写序列化的方法的话,在反序列化之后transient通常会有一个默认值,及时是在域中指定的初值在序列化之后也是不存在的。而是成为比如0,null等等的默认值。所以通常来说final和transient不会一起用,因为这样会没有办法通过常规手段赋值。如果要说一个例子的话,考虑一个map类型的缓存是满足这样的条件的。

  • 如何给final transient的域赋值

这里用的是比较hack的一类方法,使用UNSAFE或者反射,所以这里使用了UNSAFE,应该是考虑的效率的问题。
另外还有一类是使用Externalizable方法,这个方法会调用一个新的无参的构造函数。我在自身尝试的过程中也是有效的,也没有发现什么问题。但是不太明白这里为什么不使用这个。有可能是考虑到重复构造数组的缘故吧,因为这里的数组也是transient的修饰符。另外还有一个可能是这里不仅仅在序列化的部分需要重设值而且在clone的部分也需要重新设置,所以使用UNSAFE方法可以统一实现。

方法

整体的方法并没有什么特别之处,相比于concurrent包中的其他类。这个类的实现要简单很多,这里只看一个add方法

    public void add(int index, E element) 
        final ReentrantLock lock = this.lock;
        lock.lock();
        try 
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else 
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            
            newElements[index] = element;
            setArray(newElements);
         finally 
            lock.unlock();
        
    

首先获得当前数组,然后对index进行检查。然后构造一个新的数组并复制之前的所有元素到到对应位置。当然整个过程由锁lock来进行同步。

两个基本的方法:

System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) //是一个native方法,
Arrays.copyOf(T[] original, int newLength) //是一个Arrays方法会返回一个新的构造的数组。同时这个方法也会调用System.arraycopy

其他方法较为简单,这里简要说明一下:

对于非修改的方法整体的设计思想就是对于所有修改的内容都返回复制后的数组,保证原数组引用的安全性。正确的发布对象。当然如果是返回一个单独的元素则直接去索引就好

对于contains方法则使用indexof进行遍历查找。

内部类

这里有3个内部类

COWIterator

首先对于COWIterator。这个迭代只允许访问操作,所以这里可以直接将Array的数组引用直接传过来。也不会影响原数组。对象依然是正确发布的。因为Iterator既不会修改原数组的内容,也不会将引用泄露。

        private COWIterator(Object[] elements, int initialCursor) 
            cursor = initialCursor;
            snapshot = elements;
        

迭代器的构造就是传入引用和指定当前位置,
其所有修改操作均抛出NoSuchElementException异常。

COWSubList和COWSubListIterator

这两者的设计也基本雷同于非并发类容器,也就是在构造的时候指定一下上下边界,如果在进行访问操作的时候需要进行边界检测。比较常规这里不展开说明了。
唯一的区别是在subList中有一个expectedArray的Object[]类型的数组,这个数组是构造的时候传入的,然后每次进行操作的时候需要进行比较就是当前的expectedArray和Array[]是否是相同的,如果是继续否则抛出异常。

CopyOnWriteArraySet

CopyOnWriteArraySet是通过将方法委托给CopyOnWriteArrayList代理实现的。所以这里也就不细说了。
需要说明的是因为是通过数组实现的,所以例如contain之类的方法是需要用遍历数组去找的。所以不要想当然的将非同步容器的性质套给CopyOnwriteArraySet。

以上是关于源码分析-CopyOnWriteArrayList的主要内容,如果未能解决你的问题,请参考以下文章

CopyOnWriteArrayList实现原理及源码分析

「源码分析」CopyOnWriteArrayList 中的隐藏知识,你Get了吗?

死磕 java集合之CopyOnWriteArrayList源码分析

死磕 java集合之CopyOnWriteArrayList源码分析

从面试角度分析CopyOnWriteArrayList源码

从面试角度分析CopyOnWriteArrayList源码