• CopyOnWriteArrayList源码解析


    一、简介

    CopyOnWriteArrayList通过读写分离的形式重构ArrayList,保证ArrayList在循环遍历过程中的读写分离性,保证数组的最终一致性,适用于多读少写的情景下。

    二、继承体系

    ![CopyOnWrite继承体系](

    )

    CopyOnWriteArrayList实现了List,Serializable,RandomAccess,Cloneable接口

    实现List接口,为提供add,remove,get,set等操作

    实现RandomAcess接口表示该类可自由访问

    和ArrayList的继承相对比发现COWA和其先辈实现的接口基本一致,其中最本质的是Collection和List,保证这两个类能够在轻易的进行切换(面向对象的多态性)。

    1

    三、重要属性

    /** The lock protecting all mutators */
    //可重入锁,实现并发控制
    final transient ReentrantLock lock = new ReentrantLock();
    
    /** The array, accessed only via getArray/setArray. */
    //存储原始数组对象
    private transient volatile Object[] array;
    
    

    lock 实现写时加锁的控制

    Object[] array存储需要的数据

    四、重要方法解析

    主要解析包括以下几个方法

    4.1 构造函数

    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)
                //如果数组类型不属于Object[].class 就重新赋值一份数组
                if (elements.getClass() != Object[].class)
                    elements = Arrays.copyOf(elements, elements.length, Object[].class);
            }
            setArray(elements);
        }
    

    两个方式的拷贝:第一种方式是如果是相同的直接进行引用即可,第二种方式通过重新赋值数组并且进行数组元素类型的转化来实现。

    4.2 eq方法

    private static boolean eq(Object o1, Object o2) {
    	return (o1 == null) ? o2 == null : o1.equals(o2);
    }
    

    用于判断两个对象是否相等,元素可能存在null,需要重写方法不能使用类似与o1.equals(o2)的方式会导致NPE。

    4.2 set方法

    /** 替换指定index上的元素*/
     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) {//使用==而不是equals()来判断 
                    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();//归还锁
            }
        }
    
    

    主要流程:

    1. 上锁
    2. 获取index上的对象
    3. 查看两个是否相等
    4. 不相等则copy新的数组
    5. 修改新的数组并修改引用
    6. 解锁

    set方法为写方法,需要继续进行加锁,实现多个写之间的一致性。
    记录index上的元素oldValue,如果两个地址是不完全一致的就复制原有数组并进行元素的修改,最后复制回去。

    为什么使用==进行比较而不是eq(A,B)
    array中实际存储的是对象数组,array不同于hash需要使用hashcode和equals函数来进行判断

    4.3 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();
        }
    }
    
    

    主要流程:

    1. 上锁
    2. 拷贝长度为len+1的新数组
    3. 进行复制
    4. 修改引用
    5. 解锁

    从add中就能看出数组的长度变化情况,与ArrayList不同的扩容机制,COW的数组并不含有空余空间,数组完全饱和

    4.4 add(int,E)方法

    向指定位置上添加元素的方法

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

    主要流程:

    1. 上锁
    2. 确定右侧需要移动的元素
      1. 需要移动的元素为0
        1. 直接进行扩容
      2. 需要移动的元素不为0
        1. 进行数组扩容
        2. 分左右两侧进行复制
    3. 替换位置上的值
    4. 解锁

    4.5 addIfAbsent(E)方法

    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :/*先进性快速的判断*/
        addIfAbsent(e, snapshot);
    }
    
    /**
         * A version of addIfAbsent using the strong hint that given
         * recent snapshot does not contain e.
         */
    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))//判断公共的部分,如果该元素已经添加就不在添加
                        return false;
                if (indexOf(e, current, common, len) >= 0)//判断剩余的部分
                    return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    
    

    主要流程:

    • 获取镜像进行,判断快照中是否存在元素e
      • 如果存在返回false表示添加失败
      • 如果不存在需要进行加锁操作
        • 上锁
        • 将现有元素与快照的公共部分(前半部分)进行比较,如果发现元素e已经被添加到数组中就结束
        • 后半部分进行indexOf查找操作,如果发现元素e就结束
        • 进行扩容并添加新数据到结尾
        • 解锁

    其中有很多非常有意思的小细节
    第一个:先进行了无锁化的查找,看是否存在元素,当不存在时再添加。而不是直接进行上锁在判断元素是否存在
    第二个:正是由于上面的无锁化操作,导致快照和当前数组可能不一致,但依然利用上了快照信息,其中有个比较有意思的问题是没有直接使用indexOf进行重新查找,而是附加了比较查找,这里有个很底层的问题就是==!=的比较速度要比indexOf中的eq()的速度快得多,加之对于Array大部分的操作都是add或者add(index,e),如果index的值较大的话对于效率的提升会更加高

    这里addIfAbsent(index,e)需要两步:确认是否存在和添加,将第一步的查找不加锁,而第二步修改进行加锁,实属精髓

    内容来自博客园,拒绝爬虫网站
  • 相关阅读:
    A CIRCULAR PROGRESSBAR STYLE USING AN ATTACHED VIEWMODEL
    First MarkDown Blog
    Folder Recursion with C#
    39. Volume Rendering Techniques
    Service Station
    WPF Wonders: Transformations (and Robots!)
    Flipping elements with WPF
    FoLlow 的技术博客
    利用索引降低并发事务引起的锁【转】
    企业级应用架构模型-并发,事务,锁
  • 原文地址:https://www.cnblogs.com/Heliner/p/11559737.html
Copyright © 2020-2023  润新知