【问题标题】:How can CopyOnWriteArrayList be thread-safe?CopyOnWriteArrayList 如何是线程安全的?
【发布时间】:2011-02-26 09:50:50
【问题描述】:

我查看了CopyOnWriteArrayListOpenJDK source code,似乎所有的写操作都受到同一个锁的保护,而读操作根本不受保护。据我了解,在 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(new)-4-2(copyOf)-3 一样重新排序,get 可能会观察到数组处于不一致状态。

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

【问题讨论】:

    标签: java data-structures concurrency java-memory-model


    【解决方案1】:

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

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

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

    【讨论】:

    • 谢谢。我错过了数组是volatile
    • 一个重要的细节是 volatile 仅适用于数组引用本身,而不适用于数组的内容。但是,由于对数组的所有更改都是在 其引用发布之前进行的,因此 volatile 保证会扩展到数组的内容。
    • Up until this point any read operations...。那么,这是否意味着如果 COW 迭代器尚未到达该项目/点/位置,它仍然可能看到更新的值?
    【解决方案2】:

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

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

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

    【讨论】:

    • 这不是 100% 准确的。设置引用的原子性不足以保证一致性,Java 内存模型规则解决了这个问题。可能会发生乱序写入和指令重新排序,然后线程可以接收指向不一致对象的引用。双重检查锁定模式也会发生这种情况(请参阅ibm.com/developerworks/java/library/j-dcl.html
    • 不一样。对 volatile 的读/写被认为是 JMM 所称的“同步操作”,并定义了可以重新排序的障碍。见java.sun.com/docs/books/jls/third_edition/html/memory.html
    • @Eyal Schneider:欢迎来到 2004 年(参见 ibm.com/developerworks/library/j-jtp03304)。阅读标题为“volatile 的新保证”的部分
    • 没错。但是根据您的回复,看起来引用设置的原子性本身就可以保证数据的完整性。
    • 这是一个答案的细节问题。但由于这至少对一个人造成了困惑,我将更新我的答案以明确这一点。
    【解决方案3】:

    所以根据Java 1.8,以下是CopyOnWriteArrayListarraylock的声明。

    /** 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 已经提到的,array 是易变的,只能通过 setArray 方法更新。之后,如果进行了所有只读调用,那么它们将获得更新的值,因此数组在这里始终是一致的。

    【讨论】:

      【解决方案4】:

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

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

      顾名思义,CopyOnWriteArrayList 创建底层副本 ArrayList 与每个突变操作,例如添加或设置。一般 CopyOnWriteArrayList 非常昂贵,因为它涉及成本高昂 每次写入操作时都进行数组复制,但 如果您这样做会非常有效 有一个迭代超过突变的列表,例如你主要需要 迭代 ArrayList,不要经常修改。

      CopyOnWriteArrayList 的迭代器是故障安全的,不会抛出异常 ConcurrentModificationException 即使底层 一旦迭代开始,CopyOnWriteArrayList 就会被修改,因为 迭代器在 ArrayList 的单独副本上运行。因此所有 在 CopyOnWriteArrayList 上所做的更新不适用于 Iterator。

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-10-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-08-10
        • 2011-08-13
        相关资源
        最近更新 更多