【Java】ArrayList 遍历删除为什么报错?三种写法与漏删复现

遍历 ArrayList 时,条件满足就调用 list.remove(...),有时抛出 ConcurrentModificationException,有时却"看起来正常";改成正序下标循环,又可能留下该删的元素。真正的问题是遍历位置、列表结构和修改路径没有配合好。下面用单线程程序复现异常与漏删,比较 Iterator.remove()、removeIf、倒序索引和复制遍历,并核对每种方法的输出,而不只看异常是否消失。
1. 复现:单线程也会抛出 CME
新建 ArrayListRemoveDemo.java,使用 JDK 17 或更高版本,依次执行 javac ArrayListRemoveDemo.java 与 java ArrayListRemoveDemo。后续局部 Java 片段放入 main 方法,并保留需要的 import;每次对照都创建新列表:
java
import java.util.ArrayList;
import java.util.List;
public class ArrayListRemoveDemo {
public static void main(String[] args) {
List<Integer> values = new ArrayList<>(List.of(1, 2, 3, 4, 5));
for (Integer value : values) {
if (value % 2 == 0) {
values.remove(value);
}
}
System.out.println(values);
}
}
text
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(...)
增强 for 并不是一种"集合快照循环"。对实现了 Iterable 的集合,编译器会使用迭代器取元素。上面代码里,for 创建的迭代器负责记录遍历位置;values.remove(value) 却直接修改列表。一次结构性修改之后,迭代器下一次检查时发现预期状态与列表当前状态不一致,于是快速失败。下图 把两个状态的变化画在同一条时间线上。

实验结果:本机 JDK 21 的删除结果与边界输出;不是性能基准。
2. 列表与迭代器为何不同步
异常名称里的 Concurrent 容易误导。单线程中,只要一个迭代器正在工作,而代码通过集合自身的方法增删元素,就可能触发它。反过来,多线程访问普通 ArrayList 也不保证必然抛出异常:ArrayList 本身不提供线程安全,迭代器的 fail-fast 检查只是尽力发现错误,不是锁,也不是并发正确性保证。Oracle 文档明确指出,这种检查不能作为程序正确性的依赖条件。
理解它需要区分"替换元素"和"改变结构"。list.set(i, x) 只改位置上的值,不改变列表长度;add、remove 会改变元素数量和后续索引,属于结构变化。ArrayList 迭代器在创建时保留一份预期修改计数,遍历过程中检查它是否仍与列表的结构修改计数一致。直接调用 list.remove 会改变列表的计数,迭代器自身并不知道这次删除发生过;检查失败就会抛出 CME。
实现细节随 JDK 版本可能调整,但 ArrayList 的公共契约强调:迭代期间的结构修改要经由当前迭代器支持的操作。普通 Iterator 提供 remove,而 ListIterator 还提供 add。不要依赖 modCount 的具体初始值、检查发生在 next() 还是 hasNext(),更不要假定每次非法修改都会在同一行抛出异常。

教学示意:列表结构改变而迭代器预期状态未更新;计数使用示意初值。
3. 条件删除和逐项处理的两种写法
首选写法:删除条件明确时用 removeIf
如果目标是"删掉所有满足谓词的元素",Java 8 起可以直接写:
java
List<Integer> values = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
boolean changed = values.removeIf(value -> value % 2 == 0);
System.out.println(values); // [1, 3, 5]
System.out.println(changed); // true
这个 API 把"判断哪些元素应删除"和"更新列表结构"交给集合实现统一处理,表达意图清楚,也减少循环里误用外部引用修改原集合的机会。没有元素被删除时返回 false;确实有变化时返回 true。谓词应该只负责判断,不要在谓词里再次修改同一个列表、调用外部有副作用的服务或依赖不稳定状态。比如 removeIf(x -> repository.delete(x)) 把数据库写入和本地结构更新绑在一起,遇到事务失败、重复调用或部分异常就很难恢复。
若使用的是不可修改列表,removeIf 仍可能抛出 UnsupportedOperationException。例如 List.of(...) 返回不可修改列表;需要可变列表时用 new ArrayList<>(List.of(...))。集合能否修改取决于具体实现,而不是声明类型 List。
需要边遍历边作决定:Iterator.remove
当删除条件依赖复杂遍历上下文,或者每次命中后还需要做局部处理,可以显式用迭代器:
java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
List<String> names = new ArrayList<>(List.of("Ada", "", "Grace", " ", "Linus"));
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
if (name.isBlank()) {
iterator.remove();
}
}
System.out.println(names); // [Ada, Grace, Linus]
关键不是"换成 while 就行",而是删除必须通过正在推进的那个迭代器。next() 之后最多调用一次 remove();在第一次 next() 之前调用、连续调用两次或调用后还没再次 next() 就再次删除,都会得到 IllegalStateException。若有多个迭代器同时遍历同一列表,迭代器 A 的合法删除对迭代器 B 仍是外部结构修改;B 不能因此继续安全遍历。
ListIterator 还提供 add、set 等定位操作。它适合需要在当前游标附近插入或替换的列表算法;但同样要遵守操作次序约束,不能把它当成随意修改集合的许可证。
#mermaid-svg-WvCRmpxV3QO6b5oL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WvCRmpxV3QO6b5oL .error-icon{fill:#552222;}#mermaid-svg-WvCRmpxV3QO6b5oL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WvCRmpxV3QO6b5oL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WvCRmpxV3QO6b5oL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WvCRmpxV3QO6b5oL .marker.cross{stroke:#333333;}#mermaid-svg-WvCRmpxV3QO6b5oL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WvCRmpxV3QO6b5oL p{margin:0;}#mermaid-svg-WvCRmpxV3QO6b5oL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WvCRmpxV3QO6b5oL .cluster-label text{fill:#333;}#mermaid-svg-WvCRmpxV3QO6b5oL .cluster-label span{color:#333;}#mermaid-svg-WvCRmpxV3QO6b5oL .cluster-label span p{background-color:transparent;}#mermaid-svg-WvCRmpxV3QO6b5oL .label text,#mermaid-svg-WvCRmpxV3QO6b5oL span{fill:#333;color:#333;}#mermaid-svg-WvCRmpxV3QO6b5oL .node rect,#mermaid-svg-WvCRmpxV3QO6b5oL .node circle,#mermaid-svg-WvCRmpxV3QO6b5oL .node ellipse,#mermaid-svg-WvCRmpxV3QO6b5oL .node polygon,#mermaid-svg-WvCRmpxV3QO6b5oL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WvCRmpxV3QO6b5oL .rough-node .label text,#mermaid-svg-WvCRmpxV3QO6b5oL .node .label text,#mermaid-svg-WvCRmpxV3QO6b5oL .image-shape .label,#mermaid-svg-WvCRmpxV3QO6b5oL .icon-shape .label{text-anchor:middle;}#mermaid-svg-WvCRmpxV3QO6b5oL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WvCRmpxV3QO6b5oL .rough-node .label,#mermaid-svg-WvCRmpxV3QO6b5oL .node .label,#mermaid-svg-WvCRmpxV3QO6b5oL .image-shape .label,#mermaid-svg-WvCRmpxV3QO6b5oL .icon-shape .label{text-align:center;}#mermaid-svg-WvCRmpxV3QO6b5oL .node.clickable{cursor:pointer;}#mermaid-svg-WvCRmpxV3QO6b5oL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WvCRmpxV3QO6b5oL .arrowheadPath{fill:#333333;}#mermaid-svg-WvCRmpxV3QO6b5oL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WvCRmpxV3QO6b5oL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WvCRmpxV3QO6b5oL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WvCRmpxV3QO6b5oL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WvCRmpxV3QO6b5oL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WvCRmpxV3QO6b5oL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WvCRmpxV3QO6b5oL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WvCRmpxV3QO6b5oL .cluster text{fill:#333;}#mermaid-svg-WvCRmpxV3QO6b5oL .cluster span{color:#333;}#mermaid-svg-WvCRmpxV3QO6b5oL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WvCRmpxV3QO6b5oL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WvCRmpxV3QO6b5oL rect.text{fill:none;stroke-width:0;}#mermaid-svg-WvCRmpxV3QO6b5oL .icon-shape,#mermaid-svg-WvCRmpxV3QO6b5oL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WvCRmpxV3QO6b5oL .icon-shape p,#mermaid-svg-WvCRmpxV3QO6b5oL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WvCRmpxV3QO6b5oL .icon-shape .label rect,#mermaid-svg-WvCRmpxV3QO6b5oL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WvCRmpxV3QO6b5oL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WvCRmpxV3QO6b5oL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WvCRmpxV3QO6b5oL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
检查 hasNext
还有元素?
结束
next 取得元素
需要删除?
当前迭代器 remove
迭代过程:通过当前迭代器完成删除。
4. 没有异常的索引循环为何仍会漏删
对 ArrayList,需要根据索引删除时,倒序循环能避免删除后尚未访问元素的索引向前移动导致漏处理:
java
List<Integer> values = new ArrayList<>(List.of(2, 4, 6, 7, 8));
for (int i = values.size() - 1; i >= 0; i--) {
if (values.get(i) % 2 == 0) {
values.remove(i);
}
}
System.out.println(values); // [7]
这不是所有 List 都能照搬的技巧。LinkedList 的 get(i) 需要从链表一端逐步定位,循环调用 get 可能退化到 O(n²);按位置删除同样要定位。对于通用 List,若算法天然是线性扫描并按元素判断,优先用 removeIf 或迭代器。倒序索引尤其适合确实要删除指定区间、拥有明确位置且底层是随机访问列表的场景。
不要只为了"修复"把正序循环机械改成倒序。若删除规则依赖前后邻居、删除后重新检查当前项、或要维持外部索引映射,必须先明确不变量,再验证相邻命中和首尾位置。
正序循环为什么会漏删
把输入换成连续命中的 [2,4,6,7],正序循环从索引零开始删除二。列表立刻变成 [4,6,7],但循环变量接着递增到一,下一次读到的是六,不是四。删除六以后,七移动到一,循环递增到二并结束,最终留下 [4,7]。这次没有迭代器参与,也没有 CME,结果却同样错误。

教学示意:删除改变后续位置,正序索引递增越过了连续命中的元素。
正序算法并不是绝对不能写:删除后不递增当前索引,也能重新检查移动过来的元素。但这个不变量必须明确,不能一边更新 size,一边又照常递增游标。对按元素条件删除的任务,removeIf 更容易表达;按索引算法则适合你真的需要位置语义的时候。
5. 重载、视图、快照与可变性
复制遍历、Stream 和并发容器不是万能替代
若要遍历原集合并把结果写入另一个集合,可明确使用快照:
java
List<String> source = new ArrayList<>(List.of("a", "", "b"));
for (String item : new ArrayList<>(source)) {
if (item.isBlank()) {
source.remove(item);
}
}
这样迭代的是副本,修改的是源列表,不会破坏当前迭代器。但这会额外分配 O(n) 空间;remove(item) 每次还要在线性列表中查找并移动元素,频繁删除时可能产生 O(n²) 工作。若只是按条件过滤,removeIf 通常更直接。另一种方式是收集保留项到新列表,再替换引用,适合读多写少、希望保留旧列表不变的情形。
stream().filter(...).toList() 会产生过滤后的结果,但 Stream.toList() 返回的列表不可修改;若后续需要增删,可包装成 new ArrayList<>(...)。Stream 也不允许在流处理过程中偷偷修改来源集合。CopyOnWriteArrayList 的迭代器读取快照,适合读远多于写、集合规模小且允许读到旧视图的监听器列表;每次写入都复制底层数组,不适合高频删除或大列表。换容器要基于并发语义与写入比例,不是为了压住异常。
两个特别容易遗漏的边界
第一,异常不一定每次都出现。增强 for 最终大致会在每轮调用 hasNext(),有元素时再调用 next();很多 ArrayList 版本是在 next() 中检查结构修改。如果当前删除动作恰好使游标到达"没有下一项"的状态,循环可能通过 hasNext() 正常结束,没有再走到下一次检查。因此,"我删最后一个元素没报错"不是合法性证明。正确性来自修改方式符合迭代器契约,而不是碰巧没有触发检测。测试还应包含命中项位于末尾的输入,核实结果没有漏删或留下错误元素。
第二,remove 有按索引和按对象两种重载。对 List<Integer> 写 list.remove(2),编译器会选 remove(int index),删除索引 2 的元素;它并不表示删除值为 2 的对象。若要按值移除,应写 list.remove(Integer.valueOf(2))。条件删除首选 removeIf 可避开这个重载陷阱。写单元测试时,把索引和值刻意设成不同数字,例如 [10, 20, 30, 40] 删值 2 并不能暴露问题,删值 30 才能清楚地区分索引语义。
还有一种"看似合法"的写法是遍历一个列表,同时修改它的 subList 视图,或反过来修改父列表。subList 不是独立副本,而是原列表的视图;父列表或视图上的结构修改可能使另一侧后续行为不再可依赖。若确实要基于子范围筛选,直接在原列表上用范围算法并清楚定义区间,或先拷贝子列表形成快照,再一次性应用结果。集合引用相同与否、视图是否共享底层结构,要在设计中讲清楚。
最后,批量 removeAll 也要评估输入集合的查找成本。若删除集合是 ArrayList,对每个源元素都线性查找一次可能造成大量比较;把删除键先放入 HashSet 有机会降低查找开销,但需要额外空间,并受 equals / hashCode 一致性的约束。先选择符合语义的集合类型,再用代表真实数据分布的基准验证,而不是只依据理论阶数下结论。

对比图:remove 的索引重载与对象重载,分别删除位置和值。
视图与浅复制要分别测试
循环里出现两个变量,也可能仍然共享一个结构。例如把原列表赋给另一个引用,或者从原列表得到 subList,都没有创建独立容器。调试时先画出对象关系,确认正在修改的是原对象、视图还是副本,再分析计数与游标。不能仅凭变量名不同就排除外部修改。

关系图:subList 共享结构,ArrayList 副本结构独立,但元素引用仍可能共享。
还有元素对象本身的变化。复制列表以后修改某个用户对象的状态,两份列表可能看见同样的状态更新,因为浅复制只复制了引用。结构删除、对象字段修改和深复制是三个不同层次的问题;如果筛选依赖会变化的状态,多线程场景还要确认对象可见性,而不是认为复制一次数组就完全隔离。
6. 从异常栈追踪真正的修改入口
**错误一:认为只有多线程才会报错。**先搜遍历块里对源集合的 add/remove/clear/removeAll,包括被调用方法的内部实现。单线程同样能触发。
**错误二:把 hasNext() 当成安全点。**fail-fast 的检查位置是实现细节;某个输入没抛出不表示写法正确。用至少三个命中元素、命中在头部/中间/尾部的输入验证结果,而不是只观察异常是否出现。
**错误三:增强 for 里删元素后马上 break,恰好没报错就认为安全。**这依赖迭代器是否还会再次检查。未来加日志、后续处理或更换集合实现都可能改变行为。应修正修改路径,而不是躲过当前一次检查。
**错误四:改用 removeIf,却传入不可变列表。**如果异常变成 UnsupportedOperationException,检查列表来源;配置、常量和 DTO 转换可能用了 List.of 或不可修改包装。需要修改时先显式复制。
**错误五:倒序删完后遗漏外部索引。**列表位置变化会影响并行数组、表格行号、缓存索引等外部状态。删除集合元素后,还应检查所有以位置为键的旁路结构;更稳妥的数据结构可能是以稳定 ID 建索引。
**错误六:把 CopyOnWriteArrayList 当成无成本修复。**写操作会复制数组,写频繁时 CPU 和分配量上升;迭代视图也不会反映创建后发生的更新。先确认读取是否允许快照语义,再估算集合长度和写入频率。
**现场排查顺序:**先确认抛错栈是否指向 ArrayList$Itr;定位当前迭代变量的来源;全文搜索该对象在循环内外的结构性修改;检查是否存在方法调用的隐藏副作用;最后根据"删除所有命中 / 边走边改 / 按索引操作 / 并发读写"分别选择 removeIf、迭代器、索引算法或并发设计。修复后用空列表、单元素、连续命中、首尾命中和无命中五组边界数据跑测试。
7. 复杂度与并发语义如何取舍
性能、安全与选择建议
ArrayList 是动态数组。删除中间元素需要把后面的元素向前移动,单次移动最多 O(n)。循环里连续调用 remove(index),最坏情况下重复搬移,整体可能到 O(n²)。removeIf 在常见 JDK 实现里能集中完成过滤和压缩,但复杂度细节属于实现;把它当作更清楚的条件删除接口即可,不要承诺特定倍率。大列表性能敏感时用 JMH 做同一 JDK、相同数据分布的基准,预热、重复运行并观察分配;打印时间的一次性小程序不能说明吞吐优势。
若元素来自外部输入,谓词中不要构造昂贵的正则表达式或调用远端服务;先规范化与验证输入,避免意外删除合法数据。集合更新与数据库更新不在同一事务里,不能先删内存再假设数据库必然成功。多线程共享时要么使用明确同步保护整个"遍历+修改"临界区,要么采用支持相应读写语义的数据结构;仅把引用换成 Collections.synchronizedList 仍需按文档在遍历时对列表加锁。
| 需求 | 建议 | 主要边界 |
|---|---|---|
| 删除所有符合条件的元素 | removeIf |
列表必须可修改,谓词避免副作用 |
| 遍历时逐项判断并移除 | 同一个 Iterator.remove() |
每次 next() 后最多一次 |
| 以索引定位删除,底层为 ArrayList | 倒序索引 | 关注外部位置映射 |
| 遍历快照并另行修改 | 复制集合或收集新列表 | 额外 O(n) 空间 |
| 多线程读远多于写 | 评估 CopyOnWriteArrayList |
写复制成本和旧快照语义 |
按集合类型选算法:相同 List 接口不代表相同成本
List 是抽象接口,遍历/删除的实际成本由实现决定。ArrayList 通过连续数组按位置读取是 O(1),但从中间删除要移动后续元素;LinkedList 按位置访问需要沿链表移动,但持有当前迭代器时删除节点本身可以是常数级的局部链接调整。因而"倒序下标"是 ArrayList 场景技巧,不是 List 的通用最佳实践。若方法参数声明为 List<T>,实现内部不能假设一定是 ArrayList;要么按接口通用方式处理,要么明确要求随机访问并检查 instanceof RandomAccess。
同样的 removeIf 让调用方描述过滤规则,而把移动或链接维护交给实现,是抽象层的价值。集合可以覆盖默认操作以优化批量删除。代码选择应优先表达正确意图,再考虑具体实现是否有专门的高效算法。对自定义 List 实现,还要核查它的可修改性、迭代器删除支持以及 removeIf 实现;不能只因类型实现了 List 就推断所有修改方法都可用。
8. 把删除行为固定成回归测试
把边界输入写成自动化测试
手工观察控制台适合第一次复现,长期维护则应把行为固定成测试。最低限度测试:空列表、没有命中、命中一个、连续命中、只命中头尾、列表仅一个元素、列表包含重复值。对于 removeIf,断言最终序列和返回值;对于迭代器版本,断言游标没有跳过紧邻元素。若方法允许传入不可修改列表,另外断言它采用何种契约:拒绝修改并暴露异常,还是先创建可变副本。
还要为业务谓词写针对性测试。例如"删除空白字符串"应该把 null 视为删除、保留还是非法输入?name.isBlank() 对 null 会抛空指针;若数据来源可空,先决定策略:name == null || name.isBlank() 是统一删除,还是直接校验失败。谓词把异常抛出时,当前操作已经修改了多少元素也必须有明确理解。不要把外部 API 调用放在谓词中,否则重试、超时与部分副作用都难以测试。
核对删除结果与修改契约
看到 CME,先把它当作"迭代期间结构被其他路径改了"的信号,而不是"Java 随机坏了"。条件删除用 removeIf,显式迭代过程用当前迭代器的 remove,索引算法先确认随机访问与索引不变量;涉及多线程时另行设计同步。异常消失只是第一步,删除结果正确、边界输入通过、复杂度和并发语义合适,才算真正修好。
写成可重复的回归断言
条件删除修复可以用同一规则检查多组输入:最终序列里不应再有偶数,奇数的相对顺序保持不变,删除数量等于原序列中的偶数数量。这个验证比只比较 size 更强,因为误删一个奇数同时漏删一个偶数,size 也可能恰好正确。对于重复值,还要保留数量语义,不能只把结果转成 Set 比较。
如果清理发生在请求处理线程,不应捕捉 CME 然后当作成功返回。前面的删除已经可能发生,客户端重试又会接触到部分修改后的输入。先修复修改路径,再决定业务更新是否需要事务、重建新列表或统一提交;否则异常消失后仍可能留下不完整状态。
#mermaid-svg-YQo99IYFF7biyVmB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YQo99IYFF7biyVmB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YQo99IYFF7biyVmB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YQo99IYFF7biyVmB .error-icon{fill:#552222;}#mermaid-svg-YQo99IYFF7biyVmB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YQo99IYFF7biyVmB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YQo99IYFF7biyVmB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YQo99IYFF7biyVmB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YQo99IYFF7biyVmB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YQo99IYFF7biyVmB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YQo99IYFF7biyVmB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YQo99IYFF7biyVmB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YQo99IYFF7biyVmB .marker.cross{stroke:#333333;}#mermaid-svg-YQo99IYFF7biyVmB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YQo99IYFF7biyVmB p{margin:0;}#mermaid-svg-YQo99IYFF7biyVmB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YQo99IYFF7biyVmB .cluster-label text{fill:#333;}#mermaid-svg-YQo99IYFF7biyVmB .cluster-label span{color:#333;}#mermaid-svg-YQo99IYFF7biyVmB .cluster-label span p{background-color:transparent;}#mermaid-svg-YQo99IYFF7biyVmB .label text,#mermaid-svg-YQo99IYFF7biyVmB span{fill:#333;color:#333;}#mermaid-svg-YQo99IYFF7biyVmB .node rect,#mermaid-svg-YQo99IYFF7biyVmB .node circle,#mermaid-svg-YQo99IYFF7biyVmB .node ellipse,#mermaid-svg-YQo99IYFF7biyVmB .node polygon,#mermaid-svg-YQo99IYFF7biyVmB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YQo99IYFF7biyVmB .rough-node .label text,#mermaid-svg-YQo99IYFF7biyVmB .node .label text,#mermaid-svg-YQo99IYFF7biyVmB .image-shape .label,#mermaid-svg-YQo99IYFF7biyVmB .icon-shape .label{text-anchor:middle;}#mermaid-svg-YQo99IYFF7biyVmB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YQo99IYFF7biyVmB .rough-node .label,#mermaid-svg-YQo99IYFF7biyVmB .node .label,#mermaid-svg-YQo99IYFF7biyVmB .image-shape .label,#mermaid-svg-YQo99IYFF7biyVmB .icon-shape .label{text-align:center;}#mermaid-svg-YQo99IYFF7biyVmB .node.clickable{cursor:pointer;}#mermaid-svg-YQo99IYFF7biyVmB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YQo99IYFF7biyVmB .arrowheadPath{fill:#333333;}#mermaid-svg-YQo99IYFF7biyVmB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YQo99IYFF7biyVmB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YQo99IYFF7biyVmB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YQo99IYFF7biyVmB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YQo99IYFF7biyVmB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YQo99IYFF7biyVmB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YQo99IYFF7biyVmB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YQo99IYFF7biyVmB .cluster text{fill:#333;}#mermaid-svg-YQo99IYFF7biyVmB .cluster span{color:#333;}#mermaid-svg-YQo99IYFF7biyVmB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YQo99IYFF7biyVmB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YQo99IYFF7biyVmB rect.text{fill:none;stroke-width:0;}#mermaid-svg-YQo99IYFF7biyVmB .icon-shape,#mermaid-svg-YQo99IYFF7biyVmB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YQo99IYFF7biyVmB .icon-shape p,#mermaid-svg-YQo99IYFF7biyVmB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YQo99IYFF7biyVmB .icon-shape .label rect,#mermaid-svg-YQo99IYFF7biyVmB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YQo99IYFF7biyVmB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YQo99IYFF7biyVmB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YQo99IYFF7biyVmB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
先排除共享并发问题
只是条件筛选?
removeIf
需要逐项处理?
当前 Iterator.remove
索引算法:验证位置变化
选择关系:按任务与共享语义决定删除方式。