【问题标题】:How Concurrent modification exception is handled internally by CopyOnWriteArrayList/ConcurrentHashMap?CopyOnWriteArrayList/ConcurrentHashMap 内部如何处理并发修改异常?
【发布时间】:2019-04-22 12:49:01
【问题描述】:

我想在内部了解并发修改异常是如何在并发集合中处理的,例如 ConcurrentHashMapCopyOnWriteArrayList

互联网上有很多博客建议使用这两种数据结构以避免并发修改异常。但没有任何解释,并发收集如何在内部处理此异常。

有人可以对此提供更多见解吗?我需要一些详细的解释。

【问题讨论】:

  • 请注意,ConcurrentModificationException 异常的最常见来源是不是并发!最常见的情况是您在单个线程中同时迭代和修改集合。
  • 目前有 514 个问题被标记为concurrentmodification。您确定其中之一尚未回答您的问题吗?
  • 是的。这就是我提出这个问题的原因。没有单页参考
  • 不过,这不是创建新问题的好理由。我们通常做的是选择一个现有的问题并改进答案。如果答案不够好,您可以悬赏该问题或添加新答案。
  • @DanielPryden OP 的问题是为什么 CopyOnWriteArrayList 和 ConcurrentHashMap 会阻止抛出 CME。您的链接答案没有解决这个问题。我想答案确实存在。

标签: java multithreading collections concurrentmodification copyonwritearraylist


【解决方案1】:

你的问题的字面答案不是很有趣。 ConcurrentHashMapCopyOnWriteArrayList 不要抛出 ConcurrentModificationException 因为它们不包含抛出它的代码。

这不像ConcurrentModificationException 是一些低级的内在事物。 ArrayListHashMap 以及其他集合类,将 ConcurrentModificationException 扔到 帮助您。它们必须包含额外代码以尝试检测并发修改,并包含额外代码以引发异常。 ConcurrentModificationException 当其中一个类检测到某处存在错误导致对您的集合进行不安全的修改时抛出。

支持安全并发修改的类不会抛出ConcurrentModificationException,因为它们不需要。

如果您尝试调试 ConcurrentModificationException,还有很多其他问题可以帮助您回答:

【讨论】:

    【解决方案2】:

    这里是ArrayListCopyOnWriteArrayListadd()方法定义。

    数组列表:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    

    CopyOnWriteArrayList:

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

    从上面的代码可以看出CopyOnWriteArrayList在修改地图之前先锁了。这里我刚刚发布了add 方法的代码。如果您查看remove() / addAll() 或任何method which modifiesList structurally 的代码,您可以看到它在修改集合之前需要锁定。 ArrayList 的迭代器方法(例如next()/remove())也检查修改,但对于 CopyOnWriteArrayList 的迭代器方法不检查修改。例如:

    ArrayList 迭代器 next() 方法:

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
    

    CopyOnWriteArrayList 迭代器 next() 方法:

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
    

    【讨论】:

    • 在这种情况下如何处理修改计数? . modCount 的实现有什么具体的变化吗?
    • 这些代码 sn-ps 都没有显示如何避免ConcurrentModificationException。对于CopyOnWriteArrayList,这是因为内部Iterator 实现在整个迭代过程中保持对底层elements 数组的引用,因此它不会“看到”任何修改在那之后。
    • @JavaUser: modCountArrayList 检测意外并发修改的方式,这会导致它引发ConcurrentModificationException。正如 Javadoc 所说:“请注意,不能保证快速失败的行为,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬性保证。快速失败操作会尽最大努力抛出 ConcurrentModificationException。因此, 编写一个依赖此异常来确保其正确性的程序是错误的:ConcurrentModificationException 应该只用于检测错误。”
    • @Daniel,能否请您通过回答巩固并写下您的观点? .这将有助于读者了解全貌。
    • @JavaUser 我已经编辑了我的答案,请看一下。它可能会帮助你。谢谢。
    【解决方案3】:

    现在,这将回答 CopyOnWriteArrayList 如何避免需要 ConcurrentModificationException。

    当您修改集合时,CopyOnWriteArrayList 会做两件事

    1. 它可以防止其他线程通过锁定修改集合
    2. 将当前 CopyOnWriteArrayList 中的所有元素复制到一个新数组中,然后将该新数组分配给类的数组实例

    那么这如何防止 CME?标准集合中的 CME 只会作为迭代的结果而被抛出。如果在对集合进行迭代时,在同一个集合实例上执行了添加或删除操作,则会引发异常。

    CopyOnWriteArrayList 的迭代器将当前数组指定为集合的最终字段快照,并将其用于迭代。如果另一个线程(甚至是同一个线程)尝试添加到 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();
        }
    }
    

    注意线程本地 newElements 分配,完成后它将设置为类实例 volatile 数组。

    然后是迭代器,它被定义为

    static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array */
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  */
        private int cursor;
    

    所以在迭代时,我们正在读取任何修改之前的数组,并且由于没有其他线程可以修改快照,我们正在查看的 ConcurrentModificationException 不会发生。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-09-11
      • 2016-11-19
      • 1970-01-01
      • 2013-03-12
      • 1970-01-01
      相关资源
      最近更新 更多