- 编程语言
- 语言运行时
- 标准库
- 垃圾回收
- 并发编程
【免费下载链接】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。参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
message | String | 异常信息,可通过异常的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.cj | ArrayListIterator.next()(L55)、谓词修改检测(L425)等 | 迭代期间容器myVersion变化;谓词回调中执行了修改操作 |
| array_deque.cj | ArrayDequeIterator.next()(L235) | 迭代期间容器被结构性修改 |
| array_stack.cj | ArrayStackIterator.next()(L215) | 迭代期间容器被结构性修改 |
| linked_list.cj | 迭代器next()(L792)、谓词修改检测(L252) | 同上 |
| hash_map.cj | HashMapIterator.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类型抛出,捕获时按同一类型处理即可。
常见问题与规避建议
- 为什么会"边遍历边删除"报错?因为迭代器保存的是创建时刻的容器版本号,容器任何结构性修改都会使版本号变化,
next()据此判定迭代已"失效"。这不是随机错误,而是 fail-fast 的有意设计——宁可立即失败,也不返回不一致的结果。 - 遍历中想删除怎么办?使用迭代器自身的
remove()(当前支持HashMapIterator.remove());或者先收集需要删除的键,遍历结束后再统一删除。 - 视图对象还能用吗?只要容器发生修改,之前获取的 EntryView 就会失效并抛异常;视图应即取即用,不要长期持有。
- 如何获得更友好的报错信息?捕获后用
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
仓颉编程语言运行时与标准库。
相关推荐
仓颉编程语言 std.collection 并发修改异常 ConcurrentModificationException 解析
仓颉编程语言 std.collection 并发修改异常 ConcurrentModificationException 解析 导读 在仓颉编程语言的标准库 s
文档知识库教程OpenHarmony仓颉标准库 std.argopt 参数解析异常 ArgumentParseException 实战详解
仓颉标准库 std.argopt 参数解析异常 ArgumentParseException 实战详解 本篇文章聚焦仓颉标准库 std.argopt 包中唯一的
人工智能AI 技能文档开发工具仓颉标准库 argopt 解析结果结构体 ParsedArguments 完全指南
仓颉标准库 argopt 解析结果结构体 ParsedArguments 完全指南 ParsedArguments 是仓颉编程语言标准库 std.argopt
文档知识库教程OpenHarmony
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考