【JDK源码】集合之CopyOnWriteArrayList

简介

  • CopyOnWriteArrayList是ArrayList的线程安全版本,ArrayList可以参考【JDK源码】ArrayList,多线程环境下可以直接使用,无需加锁,内部也是通过数组实现,通过锁 + 数组拷贝 + volatile 关键字保证了线程安全每次对数组的修改都完全拷贝一份新的数组来修改,修改完了再替换掉老数组,这样保证了只阻塞写操作,不阻塞读操作,实现读写分离。

  • CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:

    1. 加锁;
    2. 从原数组中拷贝出新数组;
    3. 在新数组上进行操作,并把新数组赋值给数组容器;
    4. 解锁

除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到

继承体系

在这里插入图片描述

  • CopyOnWriteArrayList实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。
  • CopyOnWriteArrayList实现了List,提供了基础的添加、删除、遍历等操作。
  • CopyOnWriteArrayList实现了RandomAccess,提供了随机访问的能力。
  • CopyOnWriteArrayList实现了Cloneable,可以被克隆。
  • CopyOnWriteArrayList实现了Serializable,可以被序列化。

基本属性

/** 用于修改时加锁 */
final transient ReentrantLock lock = new ReentrantLock();

/** 真正存储元素的地方,只能通过getArray()/setArray()访问 volatile意思是一旦数组被修改,其它线程立马能够感知到*/
private transient volatile Object[] array;
  • lock

用于修改时加锁,使用transient修饰表示不自动序列化。

  • array

真正存储元素的地方,使用transient修饰表示不自动序列化,使用volatile修饰表示一个线程对这个字段的修改另外一个线程立即可见。

思考:为啥没有size字段?

构造方法

/**
 * 创建一个空数组
 */
public CopyOnWriteArrayList() {
    
    
    // 所有对array的操作都是通过setArray()和getArray()进行
    setArray(new Object[0]);
}
/**
 * 设置array
 */
final void setArray(Object[] a) {
    
    
    array = a;
}
============================================
/**
 * 以Collection集合为参数创建数组
 */
public CopyOnWriteArrayList(Collection<? extends E> c) {
    
    
    Object[] elements;
    //如果数组是CopyOnWriteArrayList类型,则将数组给到elements,注意这里是浅拷贝,两个集合共用一个数组
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
    
    
        //否则调用toArray将集合转化位数组
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        //转化后的数组不一定是Object[]类型
        //原因是:如果子类重写了toArray并将返回类型改为不是Object[]。
        if (elements.getClass() != Object[].class)
            //将elements以Object[]类型拷贝
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}
============================================
/**
 * 以数组为参数强转为Object[]类型创建数组
 */    
public CopyOnWriteArrayList(E[] toCopyIn) {
    
    
    //直接将toCopyIn数组以Object[]类型拷贝
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
  • 所有对array的操作都是通过setArray()和getArray()进行

  • 创建的数组一定要满足Object[]类型

add()

public boolean add(E e) {
    
    
    //获取当前锁,也就是调用该方法的对象
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
    
    
        // 将array数组给到elements临时数组
        Object[] elements = getArray();
        int len = elements.length;
        // 先将数组进行len+1(想当于扩容一位)的拷贝,并且以原来类型拷贝
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新数组中进行赋值,新元素直接放在数组的尾部
        newElements[len] = e;
        // 替换掉原来数组
        setArray(newElements);
        return true;
    } finally {
    
    
        // 释放锁
        lock.unlock();
    }
}
  • 加锁==>获取元素数组==>新建一个数组,大小为原数组长度加1,并把原数组元素拷贝到新数组
  • >把新数组赋值给当前对象的array属性,覆盖原数组>解锁
  • 通过加锁,来保证同一时刻只能有一个线程能够对同一个数组进行 add 操作。

都已经加锁了,为什么需要拷贝数组,而不是在原来数组上面进行操作呢?

  • volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
  • 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响。

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;
        // 如果给的index不在数组中,抛出异常
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        // 需要移动多少位元素 也就是index后面有多少元素
        int numMoved = len - index;
        // 等于0则插入最后一个
        if (numMoved == 0)
            // 直接拷贝并长度+1
            newElements = Arrays.copyOf(elements, len + 1);
        else {
    
    
            newElements = new Object[len + 1];
            // 拷贝旧数组前index的元素到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            // 将index之后的元素往后挪一位拷贝到新数组中
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        // 将元素放置在index处
        newElements[index] = element;
        setArray(newElements);
    } finally {
    
    
        lock.unlock();
    }
}
  • 添加位置不在数组索引中则抛出异常
  • 添加位置在尾部就直接一次拷贝len+1添加
  • 添加位置在中部,则将添加位置前后两次拷贝,然后将元素插入

小结

  1. 加锁:保证同一时刻数组只能被一个线程操作;
  2. 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马知道数组已经被修改
  3. volatile:值被修改后,其它线程能够立马感知最新值。

3 个要素缺一不可,比如说我们只使用 1 和 3 ,去掉 2,这样当我们修改数组中某个值时,并不会触发 volatile 的可见特性的,只有当数组内存地址被修改后,才能触发把最新值通知给其他线程的特性

get()

// 获取指定索引的元素,支持随机访问,时间复杂度为O(1)
public E get(int index) {
    
    
    return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    
    
    return (E) a[index];
}

remove()

// 删除指定索引位置的元素。
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)
            // 删除最后一个元素,直接将数组长度-1拷贝
            setArray(Arrays.copyOf(elements, len - 1));
        else {
    
    
            // 新数组长度-1
            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();
    }
}
  • 删除最后一个元素,直接一次拷贝原数组长度-1即可
  • 删除中间一个元素,将删除元素位置的前后元素两次拷贝到新数组

size()

public int size() {
    
    
    // 获取元素个数不需要加锁
	// 直接返回数组的长度
    return getArray().length;
}

为什么没有size属性呢?

因为每次修改都是拷贝一份正好可以存储目标个数元素的数组,所以不需要size属性了,数组的长度就是集合的大小,而不像ArrayList数组的长度实际是要大于集合的大小的。

比如,add(E e)操作,先拷贝一份n+1个元素的数组,再把新元素放到新数组的最后一位,这时新数组的长度为len+1了,也就是集合的size了。

总结

  • add、remove都要加锁,get不加锁。读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合;

  • 写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下

  • 读操作支持随机访问,时间复杂度为O(1);

  • 只保证最终一致性,不保证实时一致性

猜你喜欢

转载自blog.csdn.net/qq_51998352/article/details/121179142