☰
仓颉标准库 collection 并发修改异常 ConcurrentModificationException 完全指南
2026/10/14 5:21:05 网站建设 项目流程
  • 编程语言
  • 语言运行时
  • 标准库
  • 垃圾回收
  • 并发编程

【免费下载链接】cangjie_runtime

仓颉编程语言运行时与标准库。

项目地址:https://gitcode.com/Cangjie/cangjie_runtime
点击查看免费下载

导读

本文基于仓颉(Cangjie)标准库std.collection中异常类的官方 API 文档,系统讲解ConcurrentModificationException(并发修改异常)的定义、触发机制、使用方式与底层实现原理。读者将掌握:该异常在什么场景下抛出、如何在遍历容器时规避它、如何显式创建与捕获它,并能结合 concurrent_modification_exception.cj 等源码理解其"版本号(version/modCount)校验"的 fail-fast 设计。

关联文档:stdlib/doc/libs/std/collection/collection_package_api/collection_package_exception.md

异常类总览:定义与继承关系

ConcurrentModificationException是仓颉标准库 collection 包中用于报告"并发修改"问题的异常类,其完整声明如下(见 concurrent_modification_exception.cj):

public class ConcurrentModificationException <: Exception { public init() public init(message: String) }

核心要点:

  • 继承体系:该类直接继承自标准库的Exception基类,属于受检查的异常体系(关于Exception的完整定义见 core 包异常文档),因此可以在try-catch中按类型捕获。
  • 类名自描述:源码中通过protected override func getClassName(): String返回"ConcurrentModificationException",保证了toString()/message输出与类名一致。
  • 适用范围:collection 包提供的容器类(如ArrayList、HashMap、TreeMap、ArrayDeque、ArrayStack、LinkedList等)均不支持并发修改。当函数检测到不同步的并发修改时,就会抛出该异常。

术语说明:这里的"并发修改"并不仅指多线程并发。在同一线程内,先创建迭代器/视图、再于遍历过程中修改容器,同样属于"不同步的修改",会触发该异常。其本质是"结构修改发生在迭代进行中"。

典型的抛出异常场景

文档明确了两类最常见的触发场景,下面结合源码逐一展开。

场景一:for-in 遍历过程中修改容器

在for-in遍历一个容器的过程中对该容器进行修改(删除、插入、清空等结构性修改)时,绝大多数迭代器会抛出ConcurrentModificationException。唯一的例外是HashMapIterator的remove()方法——它是被官方允许的"遍历中删除"接口。

以 hash_map.cj 中的HashMapIterator为例,其next()方法在每次取元素前都会执行版本校验:

@Frozen public func next(): ?(K, V) { if (lockVersion != data.version()) { throw ConcurrentModificationException() } // ... 跳过空槽位并返回下一个键值对 }

ArrayListIterator也有同样的校验逻辑(见 array_list.cj):

@Frozen @OverflowWrapping public func next(): Option<T> { if (myData.myVersion != initVersion) { throw ConcurrentModificationException() } // ... }

TreeMap的两个方向迭代器同样通过公共的checkVersion()函数统一校验(见 tree_map.cj):

func checkVersion<K, V>(node: TreeMapEntryView<K, V>) where K <: Comparable<K> { if (node.lockVersion != node.map.version()) { throw ConcurrentModificationException() } }

fail-fast 机制的实现原理:以ArrayList为例,迭代器构造时把容器当前的版本号保存为initVersion;容器每次发生结构性修改(add、remove、clear等)都会执行myVersion++(见 array_list.cj 等多处)。当迭代器next()发现myData.myVersion != initVersion时,说明容器在迭代期间被修改过,立即抛出ConcurrentModificationException,从而避免在脏数据上继续迭代产生不可预期的结果。HashMap则使用modCount计数,并通过version()方法暴露(见 hash_map.cj)。

典型错误代码(会抛异常):

import std.collection.* main(): Unit { let map = HashMap<String, Int>() map["a"] = 1 map["b"] = 2 // 遍历过程中执行修改操作:抛出 ConcurrentModificationException try { for ((k, v) in map) { if (k == "a") { map.remove("a") // 结构性修改 } } } catch (e: ConcurrentModificationException) { println("捕获到并发修改异常: ${e.message}") } }

唯一例外——HashMapIterator.remove():HashMapIterator的remove()方法在删除后主动重新同步版本号(lockVersion = data.version()),因此不会抛出该异常(见 hash_map.cj)。也就是说,如果确实需要在遍历HashMap时删除元素,应显式使用迭代器的remove(),而不是容器自身的remove()。

场景二:生命周期较短的类型(视图类)

当使用生命周期较短的视图类型,如MapEntryView(其HashMap实现为HashMapEntryView,见 hash_map.cj)时,只要其所在的容器内容被修改,视图的后续访问就会抛出该异常。

以HashMapEntryView为例,它保存了创建时的容器版本lockVersion,其getKey()、getValue()、setValue()三个核心方法均先做版本校验:

@Frozen func getValue(): ?V { if (lockVersion != map.version()) { throw ConcurrentModificationException() } return if (_isAbsent) { None } else { map.entries[index].value } }

TreeMapEntryView也采用相同的校验模式(见 tree_map.cj),例如在next()遍历时先调用checkVersion(curNode)检查视图所依赖的容器版本是否变化。

典型错误代码(会抛异常):

import std.collection.* main(): Unit { let map = HashMap<String, Int>() map["key"] = 1 let view = map.entries[0] // 获取一个 MapEntryView(视图) map["another"] = 2 // 修改容器内容 try { println(view.key) // 视图访问触发版本校验失败 } catch (e: ConcurrentModificationException) { println("捕获到并发修改异常") } }

实践建议:视图类(EntryView / Iterator / SubList 等)应当"即取即用",在获取后不要再去修改原容器;若确实需要边遍历边增删,优先使用迭代器提供的remove()等同步方法。

构造方式一:init() —— 无参数构造

public init()

功能:构造一个不带异常信息的ConcurrentModificationException实例。在源码中对应调用父类Exception的无参构造(super()),不设置 message。

完整示例(可直接复制运行):

import std.collection.* main(): Unit { // 使用无参构造函数创建ConcurrentModificationException实例 let exception = ConcurrentModificationException() println("创建了无参的ConcurrentModificationException实例") // 可以抛出并捕获该异常 try { throw ConcurrentModificationException() } catch (e: ConcurrentModificationException) { println("捕获到ConcurrentModificationException异常") } }

运行结果:

创建了无参的ConcurrentModificationException实例 捕获到ConcurrentModificationException异常

构造方式二:init(String) —— 带异常信息构造

public init(message: String)

功能:根据异常信息构造异常实例,底层调用super(message)将 message 透传给基类Exception。参数说明:

参数类型说明
messageString异常信息,可通过异常的message属性读取

完整示例(可直接复制运行):

import std.collection.* main(): Unit { // 使用带消息的构造函数创建ConcurrentModificationException实例 let message = "并发修改异常:在遍历过程中修改了容器" let exception = ConcurrentModificationException(message) println("创建了带消息的ConcurrentModificationException实例: ${exception.message}") // 可以抛出并捕获该异常 try { throw ConcurrentModificationException("容器在遍历过程中被修改") } catch (e: ConcurrentModificationException) { println("捕获到异常: ${e.message}") } }

运行结果:

创建了带消息的ConcurrentModificationException实例: 并发修改异常:在遍历过程中修改了容器 捕获到异常: 容器在遍历过程中被修改

异常抛出点与触发条件的源码级梳理

从仓库源码可以统计出该异常在当前 collection 包中的实际抛出分布(均为throw ConcurrentModificationException()的调用点):

容器类典型抛出位置触发条件
array_list.cjArrayListIterator.next()(L55)、谓词修改检测(L425)等迭代期间容器myVersion变化;谓词回调中执行了修改操作
array_deque.cjArrayDequeIterator.next()(L235)迭代期间容器被结构性修改
array_stack.cjArrayStackIterator.next()(L215)迭代期间容器被结构性修改
linked_list.cj迭代器next()(L792)、谓词修改检测(L252)同上
hash_map.cjHashMapIterator.next()(L1352)、remove()(L1381)、HashMapEntryView的getKey/getValue/setValue(L268/L283/L303)、谓词修改检测(L675/L690)等容器modCount与视图/迭代器锁定的版本不一致
tree_map.cj正/反序遍历next()(L1150/L1171)、checkVersion()(L1313)、TreeMapIterator.next()(L1467)等同上

值得注意的细节:除迭代器与视图外,部分谓词(predicate)型 API也会抛出该异常。例如HashMap的某些以函数为参数的操作会在执行期间检测"谓词内部是否进行了修改操作",一旦发现即抛出带说明信息的异常("The predicate cannot contain a modify operation.",见 hash_map.cj)。这类异常同样以ConcurrentModificationException类型抛出,捕获时按同一类型处理即可。

常见问题与规避建议

  1. 为什么会"边遍历边删除"报错?因为迭代器保存的是创建时刻的容器版本号,容器任何结构性修改都会使版本号变化,next()据此判定迭代已"失效"。这不是随机错误,而是 fail-fast 的有意设计——宁可立即失败,也不返回不一致的结果。
  2. 遍历中想删除怎么办?使用迭代器自身的remove()(当前支持HashMapIterator.remove());或者先收集需要删除的键,遍历结束后再统一删除。
  3. 视图对象还能用吗?只要容器发生修改,之前获取的 EntryView 就会失效并抛异常;视图应即取即用,不要长期持有。
  4. 如何获得更友好的报错信息?捕获后用e.message获取异常信息;若需要自定义描述,可在业务代码中主动throw ConcurrentModificationException("自定义描述")。

小结

ConcurrentModificationException是仓颉标准库 collection 包提供的 fail-fast 并发修改检测机制的统一出口:无论是迭代器、EntryView 视图还是谓词回调场景,只要容器在"外部持有引用"期间发生了结构性修改,该异常就会在下次访问时被抛出。理解其背后"版本号(myVersion/modCount/version())快照对比"的实现原理(见 array_list.cj 与 hash_map.cj),有助于编写健壮、可预期的容器遍历代码,并在多线程或延迟处理场景中第一时间定位并发修改问题。

延伸阅读:更多 collection 包 API 细节可查阅 collection 包类文档 与 collection 包接口文档;异常的基类定义见 core 包异常文档。

  • 编程语言
  • 语言运行时
  • 标准库
  • 垃圾回收
  • 并发编程

【免费下载链接】cangjie_runtime

仓颉编程语言运行时与标准库。

项目地址:https://gitcode.com/Cangjie/cangjie_runtime
点击查看免费下载
上一篇:PhotoGIMP 完整安装指南:免费补丁让 GIMP 3 拥有 Photoshop 的熟悉布局
下一篇:GoldenDict-ng 音频播放引擎:FFmpeg 后端的演变与外部播放器配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询