59

我查看了OpenJDK 的源代码CopyOnWriteArrayList似乎所有的写操作都受到同一个锁的保护,而读操作根本不受保护。据我了解,在 JMM 下,对变量的所有访问(读取和写入)都应该受到锁的保护,否则可能会发生重新排序的影响。

例如,set(int, E)方法包含这些行(处于锁定状态):

/* 1 */ int len = elements.length;
/* 2 */ Object[] newElements = Arrays.copyOf(elements, len);
/* 3 */ newElements[index] = element;
/* 4 */ setArray(newElements);

get(int)另一方面,该方法只做return get(getArray(), index);

根据我对 JMM 的理解,这意味着get如果语句 1-4 像 1-2(new)-4-2(copyOf)-3 一样重新排序,则可能会观察到数组处于不一致状态。

我是否错误地理解了 JMM,或者是否有任何其他解释为什么CopyOnWriteArrayList是线程安全的?

4

4 回答 4

73

如果您查看底层数组引用,您会看到它被标记为volatile. 当发生写入操作时(例如在上面的摘录中),此volatile引用仅在最终语句中通过setArray. 到目前为止,任何读取操作都将从数组的旧副本返回元素。

重要的一点是数组更新是原子操作,因此读取将始终看到数组处于一致状态。

只为写操作取出锁的好处是提高了读取的吞吐量:这是因为 a 的写操作CopyOnWriteArrayList可能非常慢,因为它们涉及复制整个列表。

于 2010-06-01T15:05:34.047 回答
19

获取数组引用是一个原子操作。因此,读者要么会看到旧数组,要么会看到新数组——无论哪种方式,状态都是一致的。(set(int,E)在设置引用之前计算新的数组内容,因此在进行赋值时数组是一致的。)

数组引用本身被标记为volatile这样读者就不需要使用锁来查看对引用数组的更改。(编辑:另外,volatile保证分配不会重新排序,这将导致在数组可能处于不一致状态时完成分配。)

需要写锁来防止并发修改,这可能导致数组保存不一致的数据或更改丢失。

于 2010-06-01T15:05:14.340 回答
0

所以根据 Java 1.8,下面是CopyOnWriteArrayList中数组的声明。

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

/** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

下面是CopyOnWriteArrayList的add方法的定义

 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();
    }
}

正如@Adamski 已经提到的,数组是易变的,只能通过 setArray 方法更新。之后,如果进行了所有只读调用,那么它们将获得更新的值,因此数组在这里始终是一致的。

于 2019-10-30T09:53:14.293 回答
-2

CopyOnWriteArrayList是 Java 5 并发 API 中引入的并发 Collection 类,以及它ConcurrentHashMap在 Java 中流行的表亲。

CopyOnWriteArrayList实现 List 接口ArrayListVectorLinkedList它是一个线程安全的集合,它以与 Vector 或其他线程安全集合类略有不同的方式实现其线程安全。

顾名思义,CopyOnWriteArrayList 使用每个突变操作(例如添加或设置)创建底层 ArrayList 的副本。通常 CopyOnWriteArrayList 非常昂贵,因为每次写入操作都涉及昂贵的 Array 复制,但如果您有一个迭代次数超过突变的列表,例如您主要需要迭代 ArrayList 并且不要经常修改它,则它非常有效。

CopyOnWriteArrayList 的迭代器是故障安全的,即使在迭代开始后修改了底层 CopyOnWriteArrayList也不会引发 ConcurrentModificationException ,因为迭代器正在对 ArrayList 的单独副本进行操作。因此,对 CopyOnWriteArrayList 所做的所有更新对 Iterator 均不可用。

要获得最新版本,请重新阅读list.iterator();

话虽如此,大量更新此集合会降低性能。如果您尝试对 a 进行排序,CopyOnWriteArrayList您会看到列表抛出一个UnsupportedOperationException(排序调用集合上的集合 N 次)。仅当您进行超过 90% 的读取时才应使用此读取。

于 2020-10-25T13:49:08.627 回答