“不常用”的CopyOnWriteArrayList

官方简介:
ArrayList的线程安全变体,其中所有可变操作(add、set等)都是通过制作底层数组的【新副本】来实现的。
这通常成本太高,但当遍历操作的数量远远超过突变时,可能比其他方法更高效,当您不能或不想同步遍历,但需要排除并发线程之间的干扰时,这很有用。“快照”样式的迭代器方法使用对创建迭代器时数组状态的引用。此数组在迭代器的生存期内从不更改,因此不可能发生干扰,并且迭代器保证不会抛出ConcurrentModificationException。
迭代器不会反映自创建迭代器以来对列表的添加、删除或更改。不支持迭代器本身的元素更改操作(移除、设置和添加)。这些方法抛出不支持OperationException。
允许所有元素,包括null。
【内存一致性影响】:与其他并发集合一样,在将对象放入CopyOnWriteArrayList线程中的操作发生在另一个线程中从CopyOnWriteArray列表访问或删除该元素之后的操作之前。

ArrayList的线程安全变体,所以说我们需要学习的是为什么CopyOnWriteList线程安全。
主要看下面几个问题:

  1. add线程安全
  2. set线程安全
  3. remove线程安全
  4. 未加锁的get线程安全

源码解析

1. add线程安全

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
  	// 加锁,保证线程安全
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
      	// 注意这里copy了一份新的list,list的大小比旧的大1
      	// copy过程中不会发生线程安全问题,因为所有的对list修改的操作都共用一把锁
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
      	// 置换旧的list
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

2. set线程安全

/**
 * ÷ the element at the specified position in this list with the
 * specified element.
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E set(int index, E element) {
  	// 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);
        if (oldValue != element) {
            int len = elements.length;
          	// 如果将要set的元素和当前位置上的元素不想等
          	// 仍然copy一份新的list,在它上面操作
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
          	// 操作之后置换
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics	
          	// 相等说明不需要换
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

3. remove线程安全

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).  Returns the element that was removed from the list.
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
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;
      	// 可以看到仍然是copy新的list,然后在新的list操作,最终置换list
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            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();
    }
}

4. 未加锁的get线程安全

public E get(int index) {
    return get(getArray(), index);
}
final Object[] getArray() {
    return array;
}
// 注意这里为什么没有直接用array,而是作为参数
private E get(Object[] a, int index) {
    return (E) a[index];
}

可以看到并没有加锁,【因为所有的操作都是在list的副本操作的,所以不需要加锁】。

一个小问题

get(Object[] a, int index)方法体为什么不直接用成员变量array,而是通过参数传进去?

答:个人认为:

  1. 对于调用者而言,想要的是调用get那一刻array里面存储的数据,按照当前的写法,程序拿到了调用那一刻的array引用;如果直接用array的话,那么在get期间,可能array引用的值会被其他的修改方法给修改了,那么得到的是新的array的值,不一定符合预期
  2. 符合happens-before语意:将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作(虽然add操作在最后才将新的array赋值给成员变量array,但是后面还有return、unlock操作,从程序语意上来说,add并没有执行完)

总结

  1. 其实现原理采用”CopyOnWrite”的思路(不可变元素),即所有写操作,包括:add,remove,set等都会触发底层数组的拷贝,从而在写操作过程中,不会影响读操作;避免了使用synchronized等进行读写操作的线程同步;
  2. 写加锁同时还进行了copy,所以说 CopyOnWrite对于写操作来说代价很大,故不适合于写操作很多的场景;当遍历操作远远多于写操作的时候,适合使用CopyOnWriteArrayList;
  3. 迭代器以”快照”方式实现,在迭代器创建时,引用指向List当前状态的底层数组,所以在迭代器使用的整个生命周期中,其内部数据不会被改变;并且集合在遍历过程中进行修改,也不会抛出ConcurrentModificationException;迭代器在遍历过程中,不会感知集合的add,remove,set等操作;
  4. 因为迭代器指向的是底层数组的”快照”,因此也不支持对迭代器本身的修改操作,包括add,remove,set等操作,如果使用这些操作,将会抛出UnsupportedOperationException;
  5. 相关Happens-Before规则:一个线程将元素放入集合的操作happens-before于其它线程访问/删除该元素的操作;

适用场景

数据量较小,读操作尤其是遍历操作【远多于】写操作时候,适合使用CopyOnWriteArrayList。

你可能感兴趣的:(java)