CopyOnWriteArrayList部分源码分析

CopyOnWriteArrayList部分源码分析

​ 我们都知道ArrayList是基于数组实现的可动态扩容的集合,但是他实际上也是线程不安全的,而在JUC(java.util.concurrent)下有个线程安全的数组集合,就是CopyOnWriteArrayList,我们将从他的源码开始分析这个是怎么保证线程安全的。

构造函数

​ 在看构造函数之前,我们能看到在COWAL(简称)的类中定义了这么两个参数和两个final的方法

在这里插入图片描述

// 可以看到这里它定义了一个锁
// transient:由于这个类实现了序列化的接口,如果不想该属性被序列化,就可以加上这个关键字
final transient ReentrantLock lock = new ReentrantLock();

// 可以看到这个类仍然是基于数组实现的,并且这个数据线程间是可见的(volatile),在源码中也
// 标明这个数组只能通过getArray()/setArray()获取
private transient volatile Object[] array;

// 获取这个array
final Object[] getArray() {
    return array;
}

// 对array进行赋值
final void setArray(Object[] a) {
    array = a;
}

volatile

​ 既然看到了有volatile这个关键字,就顺便想说一下这个关键字做了什么,如果已经了解了的话可以直接跳过。

它确保了语义上对变量的读、写操作顺序被观察到

  • 对volatile变量的读、写不会被重排到对它后续的读写之后(阻止指令重排)
  • 保证写入的值可以马上同步到CPU缓存之中(写入后要求CPU马上刷新缓存)
  • 保证读取到最新版本的数据(读L3、主存等,甚至使用内存屏障)

如果逻辑上,变量的写在读之前发生,那么确保观察到的结果,写也在读之前发生。

  • 即happens-before关系
    • 如果事件A发生在事件B之前,那么观察到的结果也应如此
    • 时间关系的一致性
  • 确保可见性、有序性

我们继续他的几个构造函数的分析

// 无参构造方法就只是创建了个空的数组
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
// 这里传入的是一个集合,该集合类型为创建COWAL时的类型,就相当于addAll操作,将该集合中的
// 所有元素按照迭代器迭代出来的顺序加入到当前的数组中
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    // 判断传入的集合是否就是COWAL的类型,如果是就直接取里面的数组
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        // 不是的话先取出其中的元素数组
        elements = c.toArray();
        // 如果这个集合的类型不是ArrayList的类型,则将其复制并赋值到局部参数elements中
        if (c.getClass() != ArrayList.class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    // 将局部变量设置到当前COWAL对象的属性array中
    setArray(elements);
}
// 创建一个包含给定数组的副本列表,其实也就是将其复制并赋值到该对象的array中
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

接着我们先看看比较核心的add方法,看看是怎么保证线程安全的

// 将指定元素添加到列表的末尾
public boolean add(E e) {
    // 先对其进行加锁操作,也即当其他线程对该对象进行添加元素的操作时都要进行获取这个锁,获取不到就进入等待
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 先取出当前的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 可以看到此时是将原数组进行了复制,且将长度+1,并没有对当前数组直接进行操作
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新数组尾部加入待添加的元素
        newElements[len] = e;
        // 将新数组赋值给当前对象的array中
        setArray(newElements);
        // 返回操作成功
        return true;
    } finally {
        // 释放锁资源
        lock.unlock();
    }
}

接下来看看在指定位置加入元素是怎么操作的

// 在此列表中的指定位置插入指定元素。将当前位于该位置的元素(如果有)和任何后续元素向右移动(将其索引加一)。
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    // 同样的先进行上锁
    lock.lock();
    try {
        // 获取当前的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 对传入的index判断合法性(不允许小于0也不允许超过当前数组的大小)
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        // 仍是创建新数组
        Object[] newElements;
        // 判断当前的插入位置是否为数组尾部
        int numMoved = len - index;
        if (numMoved == 0)
            // 如果是的话直接将原数组复制到新建的数组中,并且长度 + 1
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            // 否则先将元素截止到index(范围为[0,index))复制到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            // 再将剩余的复制到index + 1开始到数组长度的位置上,实际上就是将索引向后推1位
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
       	// 空出的index存放要存放的指定元素
        newElements[index] = element;
        // 将该数组赋值回该对象的array中
        setArray(newElements);
    } finally {
        // 释放锁资源
        lock.unlock();
    }
}

​ 从上面的操作其实我们不难推测出它对删除的实际写法,无非也就是继续上锁,然后获取到当前数组,判断删除的元素的合法性以及是否为最后一位,是就将数组复制一份并将长度 - 1,然后赋值回当前的数组,不然就创建新的长度为原长度 - 1的数组并将数组从index前后拆分开,复制到新数组并剔除掉下标为index的元素,然后赋值回当前对象的array中,最后释放锁资源。

​ 可以总结出,其实对于它的写操作,总是先进行上锁,然后复制出一个新数组对其操作并将这个数组赋值回原数组,由于在读操作是不会进行上锁的,也不需要去获取锁资源,所以添加了volatile关键字使得对象属性中的array的写操作最终被读操作可见,防止出现问题。

​ 而为什么要使用写时复制呢,其实我们可以在ArrayList中看到如果在多线程并发对一个ArrayList对象同时进行读写的时候,会报错,而从源码中我们能看到读操作是没有进行加锁的,如果同样加了锁,那么效率和性能就会大大降低,而如果在并发环境下,写操作会因为锁而阻塞,但读不会,如果此时读写操作的都是同一个对象,仍是会导致不可预估的错误,所以进行了写时复制的操作,写的时候操作是对读操作透明的,但是因为volatile关键字,最终在写回数组时会保证结果对读操作是可见的,这三种操作(lock、volatile、写时复制(copyOnWrite))结合,非常精妙。

​ 以上是我个人对CopyOnWriteArrayList源码分析的个人理解,如有错误请多多指出,大家一起努力~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值