ArrayList增删改:for循环与Iterator规避ConcurrentModificationException最优性能方案
核心结论
在同步块内操作大容量ArrayList时,要规避ConcurrentModificationException且保证最优性能,核心原则是:迭代过程中仅用迭代器自身的修改方法(Iterator.remove()、ListIterator.add()/set()),非迭代场景直接调用ArrayList原生方法,全程在同步块内执行——既避免并发修改异常,又避开CopyOnWriteArrayList的全量拷贝开销。
你的理解验证
删除操作
完全正确。普通索引for循环删除中间元素会导致后续元素前移、索引错位,出现漏删或越界;而Iterator.remove()是ArrayList迭代器提供的安全删除方法,会同步更新迭代器的预期修改次数,不会触发异常,性能为O(n)(和直接遍历删除相当,但更安全)。
修正后示例代码:
Iterator<Item> itemIterator = items.iterator(); while (itemIterator.hasNext()) { Item item = itemIterator.next(); if (/* 判断是否需要删除 */) { itemIterator.remove(); } }
添加操作
- 非迭代过程直接调用
items.add(item)完全没问题,只要在同步块内,不会触发并发修改异常。 - 迭代过程中添加元素,必须用
ListIterator.add()——普通Iterator没有add方法;若用普通索引for循环添加,虽不会触发异常,但会导致迭代逻辑混乱(比如新增元素是否需要继续遍历),而ListIterator.add()会将元素插入到当前迭代位置后方,同时更新迭代状态,保证遍历逻辑可控。 - 你提到的「用普通Iterator迭代时调用
items.add()会报错」是对的,这会导致ArrayList实际修改次数与迭代器预期次数不一致,触发ConcurrentModificationException。
修改操作
性能对比
你所说的「Iterator迭代修改和增强for循环性能复杂度相同」完全正确。增强for循环本质是语法糖,底层会自动生成Iterator实现,两者时间复杂度都是O(n),实际执行效率几乎无差异——除非极端性能测试场景,否则肉眼看不出区别。
两种写法的底层字节码几乎一致:
// Iterator方式 Iterator<Item> itemIterator = items.iterator(); while (itemIterator.hasNext()) { Item item = itemIterator.next(); item.update(); // 修改元素内部属性 } // 增强for循环方式 for (Item item : items){ item.update(); }
线程安全性差异
在你假设的同步块内执行的前提下,两者线程安全性完全相同——同步块已保证同一时刻只有一个线程操作集合,无论用哪种方式,都不会出现线程安全问题。
若脱离同步块,两者都不具备线程安全性,会面临并发修改、元素可见性等问题。
附加问题:synchronizedList vs 手动同步块+ArrayList
Collections.synchronizedList(new ArrayList<>())本质是给ArrayList所有方法套上synchronized锁,相当于每个方法都在自身同步块内执行。和手动用同步块包裹ArrayList操作相比,核心区别与优势:
- 简化代码:无需手动编写
synchronized块,所有集合操作(add/remove/get等)自动同步,减少冗余。 - 覆盖全面:避免手动同步时遗漏操作(比如忘记给get方法加同步,导致读取脏数据)。
- 迭代注意事项:synchronizedList的迭代器并非线程安全,迭代过程中若有其他线程修改集合,仍会抛出
ConcurrentModificationException。因此迭代时仍需手动在外层加同步块,或使用Iterator的安全修改方法,这和普通ArrayList在同步块内的要求一致。
简言之:零散增删改用synchronizedList更省心;批量操作(如先遍历再批量修改)用手动同步块可减少锁粒度(一次锁覆盖整个批量操作,而非每个方法单独锁),性能略优——但多数场景下,这点差异可忽略。
内容的提问来源于stack exchange,提问作者Dash

