写点什么

CopyOnWriteArrayList 源码分析 - 基础和新增

作者:zarmnosaj
  • 2022 年 5 月 29 日
  • 本文字数:1743 字

    阅读完需:约 6 分钟

CopyOnWriteArrayList 源码分析-

在 ArrayList 的类注释上,记录了 ArrayList 是线程不安全的。如果要作为共享变量的话,需要自己对方法进行加锁,或者使用 Collections.synchronizedList 方法。另外,java 中还有一种线程安全的 List,是 CopyOnWriteArrayList。


是 CopyOnWriteArrayList 的特点:


  1. 线程安全,多线程环境下无需加锁可以直接使用

  2. 底层通过锁 + 数组拷贝 + volatile 保证线程安全


CopyOnWriteArrayList 在对数组进行操作的时候,基本上是四个步骤:


  1. 加锁

  2. 从原数组中拷贝出新数组

  3. 在新数组上进行操作,并把新数组赋值给数组容器

  4. 解锁


CopyOnWriteArrayList 的底层数组被 volatile 关键字修饰


private transient volatile Object[] array;
复制代码


一旦数组被修改,其它线程立马能够感知到。


整体上来说,CopyOnWriteArrayList 就是利用锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。


从 CopyOnWriteArrayList 的类注释上还可以知道:


  1. 所有的操作都是线程安全的

  2. 数组的拷贝虽然有一定的成本

  3. 迭代过程中,不会影响到原来的数组,也不会抛出 ConcurrentModificationException 异常。

尾部新增

添加元素到数组尾部:


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();    }}
复制代码


lock.lock(); 加锁


Object[] elements = getArray();得到所有的原数组


Object[] newElements = Arrays.copyOf(elements, len + 1); 拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素


newElements[len] = e; 在新数组中进行赋值,新元素直接放在数组的尾部


setArray(newElements); 替换掉原来的数组


finally { lock.unlock(); } finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁


新增总结:整个 add 过程中都是在有锁的状态下进行的,保证了同一时刻只能有一个线程能够对同一个数组进行 add 操作。


除了加锁外,还会从老数组中创建出一个新数组,然后把老数组的值拷贝到新数组上。因为数组经过了 volatile 修饰,要触发可见性,必须通过修改数组的内存地址才行。

指定位置新增

源码:


    public void add(int index, E element) {        synchronized(this.lock) {            Object[] es = this.getArray();            int len = es.length;            if (index <= len && index >= 0) {                int numMoved = len - index;                Object[] newElements;                if (numMoved == 0) {                    newElements = Arrays.copyOf(es, len + 1);                } else {                    newElements = new Object[len + 1];                    System.arraycopy(es, 0, newElements, 0, index);                    System.arraycopy(es, index, newElements, index + 1, numMoved);                }
newElements[index] = element; this.setArray(newElements); } else { throw new IndexOutOfBoundsException(outOfBounds(index, len)); } } }
复制代码


int numMoved = len - index; 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); } 如果要插入的位置在数组的中间,就需要拷贝 2 次,第一次从 0 拷贝到 index,第二次从 index+1 拷贝到末尾


newElements[index] = element; index 索引位置的值是空的,直接赋值即可。


setArray(newElements); 把新数组的值赋值给数组的容器中

总结

尾部新增和指定位置新增,可以看到 CopyOnWriteArrayList 中都通过了 加锁 + 数组拷贝+ volatile 来保证了线程安全。

用户头像

zarmnosaj

关注

靡不有初,鲜克有终 2020.02.06 加入

成都后端混子

评论

发布
暂无评论
CopyOnWriteArrayList 源码分析-基础和新增_5月月更_zarmnosaj_InfoQ写作社区