CopyOnWriteArrayList

CopyOnWriteArrayList

概述

ArrayList 为非线程安全,无法将其作为共享变量。当然 JDK 还提供了另外一种线程安全的 List,叫做 CopyOnWriteArrayList,这个 List 具有以下特征:

  • 线程安全的,多线程环境下可以直接使用,无需加锁;
  • 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;
  • 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。

相比 ArrayList ,CopyOnWriteArrayList 的属性较为简单,只有两个:

  • final transient ReentrantLock lock:多线程操作的锁控制,非公平锁;
  • private transient volatile Object[] array:存储数据的核心结构,是一个数组;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

纵观整个 CopyOnWriteArrayList 的源码实现,并没有出现数组空间大小的扩容操作。也许是出自锁的性能考虑,并没有再做扩容操作。

核心操作

初始化

无参数直接初始化

public CopyOnWriteArrayList() {
    // 做了一个赋值操作,使用空数组对象赋值
    setArray(new Object[0]);
}

final void setArray(Object[] a) {
    array = a;
}

指定初始数据初始化

public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    // c 是 CopyOnWriteArrayList,直接获取数组赋值给 elements
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        // 否则直接转发为数组,并且拷贝到 elements;
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    // 初始化 array
    setArray(elements);
}
public CopyOnWriteArrayList(E[] toCopyIn) {
    // 初始化 array
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

可以说,CopyOnWriteArrayList 和 ArrayList 的初始化并没有什么不同。理解了 ArrayList ,理解 CopyOnWriteArrayList 就非常简单了。只是 CopyOnWriteArrayList 没有指定默认空间大小的构造函数。

添加元素

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;
        // 替换掉原来的数组
        setArray(newElements);
        return true;
    // finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁   
    } finally {
        lock.unlock();
    }
}

整个 add 过程都是在持有锁的状态下进行的,通过加锁,来保证同一时刻只能有一个线程能够对同一个数组进行 add 操作。其中值得注意的是:整个操作中会从老数组中创建出一个新数组,然后把老数组的值拷贝到新数组上,这是因为 volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。如此一来,其他使用变量的线程才可以实时获取到 array 变量的变化。此外在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响。

删除元素

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)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 如果删除的数据在数组的中间,分三步走
            // 1. 设置新数组的长度减一,因为是减少一个元素
            Object[] newElements = new Object[len - 1];
            // 从 0 拷贝到数组新位置
            System.arraycopy(elements, 0, newElements, 0, index);
            // 从新位置拷贝到数组尾部
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

代码整体的结构风格也比较统一:锁 + try finally +数组拷贝,锁被 final 修饰的,保证了在加锁过程中,锁的内存地址肯定不会被修改,finally 保证锁一定能够被释放,数组拷贝是为了删除其中某个位置的元素。

public boolean removeAll(Collection<?> c) {
    if (c == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 说明数组有值,数组无值直接返回 false
        if (len != 0) {
            // newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
            int newlen = 0;
            Object[] temp = new Object[len];
            // 循环,把不包含在 c 里面的元素,放到新数组中
            for (int i = 0; i < len; ++i) {
                Object element = elements[i];
                // 不包含在 c 中的元素,从 0 开始放到新数组中
                if (!c.contains(element))
                    temp[newlen++] = element;
            }
            // 拷贝新数组,变相的删除了不包含在 c 中的元素
            if (newlen != len) {
                setArray(Arrays.copyOf(temp, newlen));
                return true;
            }
        }
        return false;
    } finally {
        lock.unlock();
    }
}

从源码中,我们可以看到,CopyOnWriteArrayList 并不会直接对数组中的元素进行挨个删除,而是先对数组中的值进行循环判断,将不需要删除的数据放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。而不是采用在 for 循环中使用单个删除的方法,单个删除的话,在每次删除的时候都会进行一次数组拷贝(删除最后一个元素时不会拷贝),很消耗性能,会导致加锁时间太长,并发大的情况下,会造成大量请求在等待锁,这也会占用一定的内存。

总结

抛开 对 array 共享变量的线程安全问题,CopyOnWriteArrayList 甚至比 ArrayList 更为简单。核心在于 CopyOnWriteArrayList 使用了 锁 + 数组拷贝 + volatile 关键字保证了线程安全。其中 数组拷贝 是为了触发 volatile 修饰变量改变的线程同步,这是整个实现的巧妙之处。

posted @ 2021-12-05 15:30  yaomianwei  阅读(28)  评论(0编辑  收藏  举报