CopyOnWriteArrayList如何保证线程安全?

63

我查看了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的理解,这意味着如果语句1-4被重新排序为1-2(新的)-4-2(copyOf)-3,则get可能会观察到不一致的状态数组。

我是否错误地理解了JMM,或者还有其他解释可以说明为什么CopyOnWriteArrayList是线程安全的?

4个回答

77

如果你查看底层数组引用,你会看到它被标记为 volatile 。当发生写操作时(比如上面的摘录),只有在最后一个语句通过 setArray 更新数组引用中的 volatile 。在此之前,任何读取操作将返回来自旧副本的数组元素。

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

仅为写操作锁定具有改进读取吞吐量的优点:这是因为复制写入整个列表可能非常缓慢。


谢谢。我错过了数组是“易失性”的事实。 - Fixpoint
22
一个重要的细节是,volatile只适用于数组引用本身,而不适用于数组的内容。但由于所有对数组的更改都是在其引用被发布之前进行的,因此volatile保证扩展到数组的内容。 - assylias
直到此时,任何读取操作都只会看到原始值。这是否意味着 COW 迭代器仍然可能看到已更新的值,如果它还没有到达该项/点/位置? - stdout

21

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

数组引用本身被标记为volatile,这样读取者就不需要使用锁来查看对引用数组的更改。(编辑:此外,volatile保证了赋值不会被重新排序,否则可能导致在数组处于不一致状态时进行赋值。)

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


1
这并不是100%准确的。设置引用的原子性并不足以保证一致性,而Java内存模型规则解决了这个问题。指令的乱序写入和重新排序可能会发生,然后线程可能会收到指向不一致对象的引用。这也会发生在双重检查锁定模式中(请参见http://www.ibm.com/developerworks/java/library/j-dcl.html)。 - Eyal Schneider
3
读写volatile变量被Java内存模型称为“同步操作”,这会对可以重排序的内容做出屏障。参考网址:http://java.sun.com/docs/books/jls/third_edition/html/memory.html。 - mdma
1
@Eyal Schneider:欢迎来到2004年(请参见http://www.ibm.com/developerworks/library/j-jtp03304/)。阅读标题为“volatile的新保证”的部分。 - Alexander Pogrebnyak
1
没错。但是根据你的回答,似乎引用设置的原子性本身就保证了数据完整性。 - Eyal Schneider
2
这只是一个关于在回答中放置多少细节的问题。但由于至少有一个人因此感到困惑,我将更新我的回答以明确说明这一点。 - mdma

1
根据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();

以下是 CopyOnWriteArrayListadd 方法定义。
 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已经提到的那样,array是易变的,并且只能通过setArray方法进行更新。之后,如果所有只读调用都被执行,它们将获得更新后的值,因此数组在这里始终保持一致。


-2

CopyOnWriteArrayList是Java 5 Concurrency API中引入的一个并发集合类,与其受欢迎的表亲ConcurrentHashMap一同出现在Java中。

CopyOnWriteArrayList实现了List接口,就像ArrayListVectorLinkedList一样,但它是线程安全的集合,并且它以稍微不同的方式实现了线程安全,而不是像Vector或其他线程安全集合类那样。

正如名称所示,CopyOnWriteArrayList在每次变异操作(例如添加或设置)时都会创建底层ArrayList的副本。通常,CopyOnWriteArrayList非常昂贵,因为它涉及到每次写操作都需要进行昂贵的数组复制,但如果您有一个迭代操作超过变异操作的列表,比如您主要需要迭代ArrayList而不经常修改它,那么它将非常高效。

CopyOnWriteArrayList的迭代器是安全的,即使在迭代开始后修改了基础CopyOnWriteArrayList,也不会抛出ConcurrentModificationException,因为迭代器正在操作ArrayList的单独副本。因此,所有对CopyOnWriteArrayList所做的更新都无法在迭代器中看到。
要获取最新版本,请像 list.iterator();一样进行新的读取。
话虽如此,频繁更新此集合将降低性能。如果您尝试对 CopyOnWriteArrayList 进行排序,您将看到列表引发 UnsupportedOperationException(排序会在集合上调用set N次)。仅当您进行90%以上的读取时才应使用此读取。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接