
09 | ConcurrentModificationException 的三种触发方式摘要ConcurrentModificationException不一定是多线程引起的。单线程增强 for 循环里删除元素也会触发本文把三种触发方式一次讲清并给出正确写法。一、问题现象publicclassCmeTest{publicstaticvoidmain(String[]args){ListStringlistnewArrayList(Arrays.asList(A,B,C,D));// 方式一增强 for 循环里删除❌ 会抛 CMEfor(Strings:list){if(B.equals(s)){list.remove(s);// ❌ ConcurrentModificationException}}}}运行结果Exception in thread main java.util.ConcurrentModificationException再看多线程版本// 方式二一个线程遍历另一个线程修改❌ 也会抛 CMEListStringlistnewArrayList(Arrays.asList(A,B,C));newThread(()-{for(Strings:list){try{Thread.sleep(100);}catch(InterruptedExceptione){}}}).start();newThread(()-{list.add(D);// 修改结构}).start();二、踩坑现场场景 1遍历时根据条件删除元素// ❌ 最常见的错误ListUserusersuserService.getUsers();for(Useruser:users){if(user.getStatus()0){users.remove(user);// ❌ CME}}场景 2Stream 里修改原集合// ❌ Stream 遍历时修改原集合ListStringlistnewArrayList(Arrays.asList(A,B,C));list.stream().forEach(s-{if(B.equals(s)){list.remove(s);// ❌ CME}});场景 3多线程「读写」同一集合即使只读不写也可能// ❌ 多线程场景即使只用迭代器遍历另一个线程修改也会失败ListStringlistnewArrayList(Arrays.asList(A,B,C));// 线程1遍历newThread(()-{for(Strings:list){System.out.println(s);}}).start();// 线程2修改newThread(()-list.add(D)).start();三、原理解析3.1 单线程 CME 的根本原因modCount 和 expectedModCountArrayList内部维护了一个modCount修改次数计数器// ArrayList 源码简化transientintmodCount0;// 结构修改次数publicbooleanadd(Ee){modCount;// 每次结构修改 1// ...}publicbooleanremove(Objecto){modCount;// 每次结构修改 1// ...}迭代器的checkForComodification检查// ArrayList 的迭代器Itr 内部类intexpectedModCountmodCount;// 迭代器创建时记录当时的 modCountfinalvoidcheckForComodification(){if(modCount!expectedModCount)thrownewConcurrentModificationException();}触发流程创建迭代器 → expectedModCount 3假设 ↓ 调用 list.remove() → modCount 变成 4 ↓ 迭代器下一次 next() → checkForComodification() ↓ modCount(4) ! expectedModCount(3) → 抛 CME3.2 增强 for 循环的本质// 你写的代码for(Strings:list){list.remove(s);}// 编译后等价于IteratorStringitlist.iterator();while(it.hasNext()){Stringsit.next();list.remove(s);// ❌ 直接调用集合的 remove迭代器不知道}关键增强 for 循环底层用迭代器遍历但list.remove(s)是直接调用集合的方法迭代器里的expectedModCount没有更新。3.3 多线程场景ArrayList不是线程安全的即使只有一个线程遍历另一个线程修改也会触发 CME这是fail-fast机制。fail-fast迭代器发现集合在遍历期间被修改立即抛异常而不是继续返回错误数据。四、正确写法4.1 单线程遍历删除用迭代器的remove()// ✅ 正确写法ListStringlistnewArrayList(Arrays.asList(A,B,C,D));IteratorStringitlist.iterator();while(it.hasNext()){Stringsit.next();if(B.equals(s)){it.remove();// ✅ 用迭代器的 remove会同步更新 expectedModCount}}4.2 Java 8用removeIf// ✅ 最简洁的写法ListStringlistnewArrayList(Arrays.asList(A,B,C,D));list.removeIf(B::equals);// ✅ 内部用迭代器实现4.3 多线程场景用并发集合// ✅ 方案1CopyOnWriteArrayList读多写少场景ListStringlistnewCopyOnWriteArrayList(Arrays.asList(A,B,C));// 遍历时不受修改影响遍历的是快照// ✅ 方案2加锁ListStringlistnewArrayList();synchronized(list){for(Strings:list){...}}4.4 边遍历边收集最后批量删除// ✅ 避免遍历时删除先收集再批量删除ListStringlistnewArrayList(Arrays.asList(A,B,C,D));ListStringtoRemovenewArrayList();for(Strings:list){if(B.equals(s)){toRemove.add(s);}}list.removeAll(toRemove);// ✅ 遍历结束后再删除五、最佳实践✅ 避免 CME 的 4 条规则遍历时删除元素必须用迭代器的remove()方法优先用removeIf()Java 8代码最简洁多线程场景用CopyOnWriteArrayList或Collections.synchronizedListforeach循环里不要调用集合的add/removeforeach不能修改集合的通用规则操作foreach里正确方式删除元素❌迭代器remove()/removeIf()添加元素❌先收集再addAll()修改元素内容✅直接修改对象属性不涉及结构变化️ 阿里巴巴 Java 开发手册规约【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式如果是并发操作需要对Iterator对象加锁。六、小结ConcurrentModificationException有三种触发方式单线程遍历修改、多线程并发读写、Stream 里修改原集合单线程触发原因modCount ! expectedModCountfail-fast 机制增强 for 循环底层是迭代器但直接调用list.remove()不会通知迭代器正确做法用Iterator.remove()或removeIf()多线程场景用CopyOnWriteArrayList或对操作加锁下一篇预告HashMap 扩容时发生了什么死循环已成历史但坑还在—— JDK 1.7 的 HashMap 多线程扩容会导致死循环1.8 修复了但依然有坑。