为什么 CopyOnWriteArrayList 需要写入和读取操作的副本?

Why CopyOnWriteArrayList needs copies for both write and read operations?

来自这个article,它说:

When we are using any of the modify methods – such as add() or remove() – the whole content of the CopyOnWriteArrayList is copied into the new internal copy.

Due to this simple fact, we can iterate over the list in a safe way, even when concurrent modification is happening.

When we're calling the iterator() method on the CopyOnWriteArrayList, we get back an Iterator backed up by the immutable snapshot of the content of the CopyOnWriteArrayList.

Its content is an exact copy of data that is inside an ArrayList from the time when the Iterator was created. Even if in the meantime some other thread adds or removes an element from the list, that modification is making a fresh copy of the data that will be used in any further data lookup from that list.

接下来要问自己一个简单的问题是为什么两者都是?基本上,根据我的理解,写入操作是在新副本上进行的,而读取操作是在集合的克隆上完成的。

例如,如果写入是在新副本上完成的,这意味着我可以迭代“原始”集合 - 这意味着它不会受到影响。那么为什么要增加在另一个副本(快照)中存储元素的开销呢?或者相反的方向,如果我将元素存储在副本(快照)中,为什么需要在副本上进行写入,而我实际上是在克隆而不是“原始”集合上迭代(意味着快照永远不会改变)?

我希望这个问题是合法的,因为我确实检查了互联网上所有可能的来源,但没有一篇文章能帮助我消除这种困惑。我在这里错过了什么?

当您调用 iterator 时,

CopyOnWriteArrayList 不会创建数组的副本,正如 docs 所说:

The "snapshot" style iterator method uses a reference to the state of the array at the point that the iterator was created.

注意“参考”这个词。

这句话写得比较烂:

Its content is an exact copy of data that is inside an ArrayList from the time when the Iterator was created.

这并不意味着当您调用 iterator() 创建了 数组的副本。应该是:

Its content is the same as the data that is inside an ArrayList from the time when the Iterator was created.

该段比较重要的一点是:

Even if in the meantime some other thread adds or removes an element from the list, that modification is making a fresh copy of the data that will be used in any further data lookup from that list.

这意味着如果您创建一个迭代器,然后继续以某种方式改变列表,迭代器将看不到这些更改。为什么?因为突变是通过创建一个具有突变的新数组来完成的,但是迭代器正在遍历没有突变的旧数组。这就是为什么我们说迭代器拍摄“快照”。

这里有一些来自 OpenJDK 的代码来说明。

iterator(), it simply creates a COWIterator with getArray()中,通过返回volatilearray字段获取快照:

final Object[] getArray() {
    return array;
}

...

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

和增变器方法,例如add,设置array字段:

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

...

public boolean add(E e) {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
}

我删除了(解锁)锁定代码,以便更容易看到发生了什么。