偶现下标越界主因是多线程并发、遍历中修改或边界计算不严谨;应杜绝遍历时结构性修改,改用iterator.remove()或倒序删除,严格校验下标范围,并优先采用不可变设计。

ArrayList 偶现下标越界(IndexOutOfBoundsException),通常不是写法明显错误,而是多线程并发、迭代中修改、或边界计算不严谨导致的“竞态”问题。直接加 synchronized 或改用 Vector 往往治标不治本,关键在定位真实触发路径。
检查是否在遍历过程中修改了列表
这是最常见诱因:一边用 for-each 或 iterator.next() 遍历,一边调用 add()、remove()、clear() 等结构性修改方法,会触发 ConcurrentModificationException;但若使用普通 for 循环 + 手动下标访问(如 list.get(i)),而另一线程/逻辑同时增删元素,就可能让原下标在下次访问时已越界。
- 确认所有遍历逻辑是否严格遵循“只读”原则;若需边遍历边删,改用
Iterator.remove(),或先收集待删索引再倒序删除 - 搜索代码中类似
for (int i = 0; i 的结构,检查循环体内部或被调用方法里是否隐式修改了该 list - 特别注意异步回调、事件监听器、定时任务中对同一 ArrayList 的访问——它们容易脱离主流程控制
排查多线程共享未同步场景
ArrayList 本身不是线程安全的。多个线程同时读写(哪怕只是读+读+一个写),都可能导致 size 缓存与实际数组长度不一致,使 get(i) 在 i
- 用 IDE 全局搜索该 ArrayList 实例的声明位置和所有引用点,确认是否被多个线程(包括线程池、CompletableFuture、Swing EDT 等)共同持有
- 临时加日志:在 get 前打印
list.size()和访问下标 i,观察异常发生前是否出现i >= list.size()的瞬间不一致 - 修复方式优先选不可变设计:尽早转为
List.copyOf(list)(Java 10+)供只读使用;必须读写共享时,用Collections.synchronizedList(new ArrayList()),且注意迭代时需手动同步整个块
审查动态下标计算逻辑
偶现也常源于业务逻辑中“看似安全”的下标推导,比如根据用户输入、网络响应、时间戳等算出索引,但未兜底校验或存在浮点误差、整数溢出、负值截断等问题。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 检查所有
list.get(xxx)前是否有显式判断:if (index >= 0 && index ;禁止依赖“前面刚 check 过 size”就跳过本次校验 - 若下标来自除法、取模、随机数(如
new Random().nextInt(list.size())),确认 list 非空——size()==0时nextInt(0)会抛IllegalArgumentException,但有些自定义算法可能返回负数或超限值 - 日志中记录触发异常的完整上下文:当前 list 大小、要访问的下标、相关输入参数(如分页页码、ID、时间范围),比单纯看堆栈更有定位价值
利用工具辅助捕获瞬时状态
偶现问题难复现,靠人工加 log 效率低。可借助 JVM 级手段抓取异常现场:
- JVM 启动参数加入
-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError(虽非 OOM,但配合-XX:OnError="jstack %p"可在异常时自动 dump 线程栈) - 用 Arthas 的
watch命令监控目标 ArrayList 的get方法调用:watch com.example.MyClass targetList.get '{params,throwExp}' -n 5,捕获最近几次调用参数与异常 - 单元测试中模拟高并发:用
Executors.newFixedThreadPool(10)启动多线程反复执行疑似代码段,配合CountDownLatch控制节奏,加速暴露问题
不复杂但容易忽略——偶现越界背后往往是状态管理松散或线程边界模糊。聚焦访问链路、守住临界校验、用工具代替猜测,比盲目加锁更有效。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










