今天收到业务方反馈,调用接口有异常发生,而且随着流量增大,异常也增多了。小白赶紧查看监控日志,发现ArrayIndexOutOfBoundsException数组越界异常变多了。于是开始进行排查。
排查时通过回放有问题的请求参数,生产环境的接口有时会报异常,有时是正常的,明明请求参数是一样的,为什么结果会不同呢。小白猜测可能是多线程使用的问题
先来看看系统逻辑
再来看看相关代码,这里小白用了模拟的方式写多线程那块的代码,并非真实业务代码
List<Integer> idList = Lists.newArrayList();
for (int i = 0; i < 100000; i++) {
idList.add(i);
}
List<Integer> result = Lists.newArrayList();
//多线程方式进行id操作,并把结果都放到result中
idList.parallelStream().forEach(id -> {
result.add(id);
});
System.out.println(result.size());
运行这段代码,就会出现ArrayIndexOutOfBoundsException异常
问题解决
通过查看代码可以发现result变量是ArrayList类型,非线程安全的,多个线程同时往ArrayList里插入数据就会发生多线程问题。解决办法也很简单,对result变量加锁,每次插入数据时都需要获取锁,就能解决并发问题了。
idList.parallelStream().forEach(id -> {
//对result变量加锁
synchronized (result) {
result.add(id);
}
});
注意:以上方式虽然保证了数据的一致性,但是并行流的效率可能会受到影响,因为多个线程需要等待锁的释放才能执行操作。可以使用并发安全的容器类ConcurrentLinkedQueue,避免多线程操作List时出现的线程安全问题。同时,使用Stream的collect方法将结果收集到ConcurrentLinkedQueue中。
代码优化如下:
ConcurrentLinkedQueue result = idList.parallelStream() .collect(Collectors.toConcurrentLinkedQueue());
问题分析
问题解决了,但是为什么报的错误是数组越界异常呢?我们知道ArrayList是有自动扩容机制的,数组放不下了,会自动扩容的,不应该会报异常啊。带着疑问,小白又翻了翻ArrayList的源代码查看
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//这儿就是关键点,两个线程先后到达这儿,检查都发现不需要扩容,第一个线程在执行下面的步骤的时候没有问题,第二个就会数组越界了。
elementData[size++] = e;
return true;
}
通过上面的代码可以看出,两个线程在add元素时,会执行ensureCapacityInternal方法判断是否扩容。判断都不需要扩容,但数组就剩一个空位时,第一个线程执行了添加操作没有问题。此时第二个线程再执行添加操作就会发生数组越界了
小白分析完问题后,不得不感叹,没想到经常用的ArrayList也有自己不知道的问题。不过也正因为有这次踩坑经历,小白对并发编程的了解又多了一些。
-------end-----------