Android应用绘制航点时触发ConcurrentModificationException问题求助
这个问题我碰到过好多次了,本质是ArrayList的快速失败迭代器机制在搞事情——哪怕你没在迭代循环里直接改列表,只要另一个线程(比如处理触摸事件的UI线程)在你迭代的时候修改了列表的结构(比如添加、删除元素,或者调整元素位置导致列表 resize),迭代器检测到内部的修改计数不一致,就会抛出这个异常。
下面给你几个靠谱的解决办法,你可以根据自己的场景选:
1. 改用线程安全的CopyOnWriteArrayList
这是最省心的方案,适合读多写少的场景(比如航点绘制的频率比用户移动航点的频率高)。CopyOnWriteArrayList每次修改操作(add/remove/set)都会复制一份底层数组,迭代器是基于修改前的旧数组创建的,所以完全不会因为后续的并发修改抛出异常。
替换起来也简单,把原来的ArrayList<Waypoint>声明改成:
List<Waypoint> waypoints = new CopyOnWriteArrayList<>();
之后不管是绘制迭代还是触摸修改,都不需要额外加锁,集合本身会处理线程安全问题。
2. 给所有列表操作加同步锁
如果不想换集合类,那就用同步锁保证同一时间只有一个线程能访问航点列表。先定义一个全局的锁对象:
private final Object waypointLock = new Object();
然后在绘制航点的迭代代码外面包上同步块:
synchronized(waypointLock) { for (Waypoint wp : waypoints) { // 你的航点绘制逻辑 drawWaypoint(wp); } }
同时,在用户触摸移动航点修改列表的代码里,也要用同一个锁包裹:
synchronized(waypointLock) { // 比如移除旧位置的航点,添加到新位置 waypoints.remove(selectedWaypointIndex); waypoints.add(newWaypointIndex, updatedWaypoint); }
这样就能彻底避免并发访问的冲突,缺点是锁会带来一点性能开销,但对于航点这种量级的数据来说基本可以忽略。
3. 迭代时使用列表的快照副本
如果对实时性要求不是特别高(比如用户移动航点后,延迟一帧绘制最新状态没问题),可以每次绘制前先复制一份航点列表的快照,然后迭代这个副本:
// 复制出一个快照,原列表修改不影响这个副本 List<Waypoint> waypointSnapshot = new ArrayList<>(waypoints); for (Waypoint wp : waypointSnapshot) { drawWaypoint(wp); }
这种方式不需要加锁,也不用换集合,缺点是绘制的可能不是最新的航点状态,适合修改频率低的场景。
额外注意点
如果你的移动航点操作只是修改航点对象的属性(比如更新坐标),而不是修改列表的结构(add/remove),那其实不会触发这个异常。这时候你要检查是不是代码里不小心做了列表结构的修改(比如为了调整航点顺序做了remove再add),或者要确保航点对象的属性修改是线程安全的(比如用volatile修饰坐标字段,或者修改属性时加锁)。
内容的提问来源于stack exchange,提问作者juergen d

