CopyOnWriteArrayList 源码分析-基础和新增

449 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情

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 来保证了线程安全。