Java 中的并发修改异常2024年10月23日 | 11 分钟阅读 Java 中的 ConcurrentModificationException 是一个异常,它告诉我们,当集合在迭代其任何元素时被结构性地修改了。这通常发生在迭代器(例如 Iterator、ListIterator)遍历集合时,集合被修改(例如添加或删除元素)。 让我们更详细地探讨一下 1. 并发和迭代 在 Java 中,集合可能会被并发修改,即一个线程正在修改集合,而另一个线程正在遍历它。然而,Java 集合框架默认并非线程安全。这意味着,如果您尝试在另一个线程正在迭代集合时修改它,您将遇到 ConcurrentModificationException 这样的问题。 2. ConcurrentModificationException 的原因 ConcurrentModificationException 最常见的情况是,当您使用迭代器(例如 Iterator、ListIterator)遍历集合,同时又在迭代器之外修改集合(即添加/删除元素)时。迭代器负责跟踪集合的内部结构。如果它检测到集合的结构在它之外被更改了,它就会抛出 ConcurrentModificationException,这意味着当前的迭代是无效的。 ConcurrentModificationException 的示例注意:此异常并非仅在某个其他线程尝试修改 Collection 对象时才会抛出。如果单个线程调用了一些违反对象约定的方法,也可能发生这种情况。当一个线程在被某个“快速失败”迭代器迭代 Collection 对象时尝试修改它,迭代器就会抛出异常。示例输出 ![]() 这条消息表明,当迭代器在遍历列表时调用 next() 方法,而我们同时对其进行修改时,就会抛出异常。但如果我们像下面这样修改 HashMap,就不会抛出任何异常,因为 HashMap 的大小不会改变。 例如: 输出 Map Value:1 Map Value:2 Map Value:3 这个例子完全正常,因为在迭代器遍历 map 时,map 的大小并没有改变。只有在 if 语句中更新了 map。 ConcurrentModificationException 的构造函数ConcurrentModificationException 有 4 种构造函数:
如何在多线程环境中避免 ConcurrentModificationException?为了在多线程环境中避免 ConcurrentModificationException,我们需要采用技术来同步对这些共享集合的访问,或者使用线程安全的数据结构。以下是一些实现此目的的技术: 1. 使用线程安全集合 Java 在 java.util.concurrent 包中提供了许多线程安全集合类,如 ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue 等,它们被设计为在多线程环境中安全使用,无需外部同步。 示例 2. 同步访问 如果您使用的是常规集合(例如 ArrayList、HashMap 等),则必须使用显式阻塞/同步(例如使用 synchronized 关键字或 ReadWriteLock)手动同步它们的访问。 示例 3. 迭代器的 remove() 方法 在遍历集合时,请使用迭代器的 remove() 方法,而不是直接修改集合。这可以确保所有修改都通过迭代器安全地进行。 示例 4. 复制集合 如果集合的大小很小,并且对其进行迭代的频率不高,那么您应该考虑在修改它之前创建一个副本。这样,您可以安全地重构副本,而无需检查并发修改。 5. 使用不可变集合 如果集合在创建后不需要修改,请考虑使用 Google Guava 库或 Java 9+ 的 List.of()、Set.of() 等提供的不可变集合。不可变集合可以在线程之间安全共享,无需同步。 示例 6. 原子操作 对于特殊情况,您可以使用原子操作,这些操作由 AtomicInteger、AtomicLong 等类提供,以实现线程安全的修改。 示例 7. 正确的线程同步 正确的线程同步可以避免并发修改问题。可以使用 synchronized blocks、Lock 对象等同步构造来控制对共享数据的访问。 ConcurrentModificationExample.java 输出 Added: Element 0 Added: Element 1 Added: Element 2 Added: Element 3 Added: Element 4 4.2 如何在单线程环境中避免 ConcurrentModificationException?在单线程环境中,您通常不会遇到与多线程环境相同的并发问题。但是,如果您尝试在迭代集合时修改它,您仍然可能遇到 ConcurrentModificationException。以下是在单线程环境中避免此问题的方法: 1. 使用迭代器的 remove() 方法 2. 使用 for-each 循环 在 Java 5 及更高版本中,您可以使用增强 for 循环,它在内部使用迭代器,并允许安全地删除元素。 3. 使用 ListIterator 如果您需要双向遍历列表并对其进行修改,可以使用 ListIterator。 4. 复制集合 如果您需要在迭代时修改集合,请考虑创建集合的副本,然后迭代副本。这样,修改就不会影响原始集合。 5. 使用 Stream API 从 Java 8 及更高版本开始,您可以使用 Stream API 安全地操作集合。 AvoidConcurrentModification.java 输出 After removing 'B' with Iterator: [A, C] After removing 'C' with for-each loop: [A] After removing 'A' with ListIterator: [] After removing 'B' by copying: [] After removing 'A' with Stream API: [] 5. 调试 ConcurrentModificationException 发生此类问题时,您需要分析代码,找出代码的哪个部分在迭代集合的同时并发修改了它。一旦找到问题,您就可以选择上述策略之一来解决它。 ConcurrentModificationExample.java 输出 Processing element: A Processing element: B Modifying the list while iterating... Modified list: [A, C] Processing element: C ConcurrentModificationException 的优点检测并发修改: ConcurrentModificationException 有助于识别在迭代期间集合被同时修改的场景。它充当早期预警系统。因此,可以识别服务过载的风险。 维护集合的完整性: 在检测到冲突修改时抛出异常,可以确保集合保持一致。这消除了在同时修改和迭代期间可能出现的小错误和数据完整性问题。 防止未定义行为: 同时,在迭代过程中处理同一个集合时,可能会出现不可预测的行为和数据不一致。ConcurrentModificationException 通过顺序访问或同步它们来避免这些情况。 鼓励安全的迭代实践: 遇到 ConcurrentModificationException 会促使程序员应用安全的迭代技术,例如正确处理迭代器、使用线程安全集合或在多线程环境中同步对集合的并发访问。 便于调试: 当抛出 ConcurrentModificationException 时,它会提供关于并发修改发生的代码位置的重要信息。这反过来又便于调试和解决与同时访问相关的问题。 推广最佳实践: ConcurrentModificationException 提醒并发编程的开发人员保持良好的实践,包括使用同步机制、采用线程安全的数据结构以及理解并发访问的后果。 增强代码可读性:当在迭代期间检测到并发修改时,明确抛出 ConcurrentModificationException 可以使代码更具可读性和自解释性。它向开发人员明确表明,在并发修改集合时正在进行迭代。这种清晰性有助于理解代码库,并降低意外引入并发修改相关错误的风险。 促进纠正措施: 当引发 ConcurrentModificationException 时,它会促使开发人员审查并可能重构其代码,以更有效地处理并发修改。这可能涉及重新设计数据结构、实现适当的同步机制或重新思考并发模型。通过这种方式,异常充当了提高多线程环境中代码的健壮性和可靠性的催化剂,从而带来更好的长期可维护性和性能。 ConcurrentModificationException 的缺点运行时异常: ConcurrentModificationException 是一个运行时异常,因此在编译期间无法检测到,如果未处理,可能会导致生产环境中意外崩溃。这种特性带来了重大风险,因为与并发修改相关的问题可能会意外出现,而开发过程中没有任何预先的指示。如果没有主动处理,这些运行时错误会破坏应用程序的稳定性和用户体验,这凸显了实施健壮的机制来提前检测和管理并发修改的重要性,从而最大程度地减少破坏性运行时异常的可能性。 信息有限: ConcurrentModificationException 异常的消息可能并不总是提供足够详细的信息来准确识别并发修改的根本原因。开发人员有时可能需要使用调试技术来查明确切的问题。 性能开销: 当抛出 ConcurrentModificationException 时,会伴随堆栈跟踪生成和异常处理过程的相当大的开销。如果并发修改频繁发生,这会严重影响应用程序性能。 潜在的静默失败: 在某些情况下,可能不会触发 ConcurrentModificationException,导致静默失败,即并发修改未被检测到。这种情况带来了严重的风险,因为未被注意到的并发修改可能导致细微的错误和数据损坏。这种静默失败很难识别和纠正,可能会对数据完整性和应用程序可靠性造成长期损害。如果没有异常提供的明确指示,这些隐藏的问题可能会持续存在,从而加剧故障排除和纠正的难度,最终损害系统的稳定性和功能。 在多线程中的实用性有限: ConcurrentModificationException 可以检测单线程环境中的并发修改,但在多线程环境中效果较差,因为它假设多个线程之间不会发生并发修改。这种使用情况需要更高水平的同步。 潜在的不可预测行为: 在某些情况下,遇到 ConcurrentModificationException 可能会导致应用程序行为不可预测。根据操作的时序和顺序,异常可能被不一致地抛出或根本不抛出。这种不可预测性使得开发人员难以可靠地重现和诊断与并发修改相关的问题。 处理并发修改的难度: 虽然 ConcurrentModificationException 表明发生了并发修改,但它本身并不提供处理或解决该问题的解决方案。开发人员仍需自己实现管理并发访问集合的机制,这可能很复杂且容易出错。缺乏内置的并发控制支持可能会导致次优解决方案,或者需要大量工作来实现健壮的并发管理策略。 下一个主题Java 教程 |
在数组中计算每个查询的最大 XOR 值的问题是一个非常有趣的话题,它涉及到位操作技术和 Trie(前缀树)数据结构。我们得到一个名为 nums 的非负整数数组……
阅读 10 分钟
当 Java 中使用两个或多个引用指向同一个对象时,这被称为“别名”。当用户向对象写入内容,而其所有者不希望在多个引用存在的情况下发生更改时,别名就会成为问题。这里,别名代码……
阅读 3 分钟
在本节中,我们将学习什么是十边形数,并创建计算十边形数的 Java 程序。十边形数程序经常在 Java 编码面试和学术界中被问到。十边形数:十边形数是形数,其递归定义为:D(n)...
5 分钟阅读
LinkedList(链表)是计算机科学中的基本数据结构,它提供动态存储分配和灵活性。它由通过指针连接的节点组成,每个节点包含数据和指向下一个节点的引用。在本文中,我们将探讨比较两个链表的各种方法……
11 分钟阅读
问题陈述:给定一个正整数 k。我们必须找到一个最小的正整数 n 的长度,该整数可被 k 整除,并且 n 中的每个数字都只包含数字 1。整数 n 应通过重复数字 1 来构建……
18 分钟阅读
Java 是一种流行的面向对象的、基于类的编程语言。Java 类是蓝图或模板,用于指定程序对象的属性和操作。这些对象的一个或多个方法中的操作可以定义 Java 类的对象。我们将详细介绍……
阅读 3 分钟
给定字符串 str,我们的任务是编写一个 Java 程序来确定提供的字符串是否为 pangram(全字母句)。如果字符串不区分大小写(大写或小写)而包含所有字母字符,则该字符串称为……
阅读 6 分钟
在本节中,我们将通过适当的示例讨论什么是 zigzag 数组(锯齿形数组)。我们还将创建一个 Java 程序来将普通数组转换为 zigzag 数组,反之亦然。什么是 zigzag 数组?一个数组称为……
阅读 6 分钟
消息编码是一种技术,用于使用各种算法将消息转换为不同的格式,以确保机密性和安全性。消息编码的一种有趣方法是使用矩阵乘法。基本思想是将消息表示为……
阅读 4 分钟
在二叉树中,显示奇数层节点(任意顺序)。假设根节点位于第 1 层。对于下面的二叉树:奇数层节点为:20 25 3 5 7。由于我们必须以任意顺序显示节点。因此,20 25 5……
阅读 4 分钟
我们请求您订阅我们的新闻通讯以获取最新更新。
我们提供所有技术(如 Java 教程、Android、Java 框架)的教程和面试问题
G-13, 2nd Floor, Sec-3, Noida, UP, 201301, India