记一次ArrayList使用不当引起的并发问题

简介: 小卷今天收到业务方反馈,调用接口有异常发生,而且随着流量增大,异常也增多了。小卷赶紧查看监控日志,发现ArrayIndexOutOfBoundsException数组越界异常变多了。于是开始进行排查。

小卷今天收到业务方反馈,调用接口有异常发生,而且随着流量增大,异常也增多了。小卷赶紧查看监控日志,发现ArrayIndexOutOfBoundsException数组越界异常变多了。于是开始进行排查。

排查时通过回放有问题的请求参数,生产环境的接口有时会报异常,有时是正常的,明明请求参数是一样的,为什么结果会不同呢。小卷猜测可能是多线程使用的问题

先来看看系统逻辑

1.png

再来看看相关代码,这里小卷用了模拟的方式写多线程那块的代码,并非真实业务代码

                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异常

2.png

问题解决

通过查看代码可以发现result变量是ArrayList类型,非线程安全的,多个线程同时往ArrayList里插入数据就会发生多线程问题。解决办法也很简单,对result变量加锁,每次插入数据时都需要获取锁,就能解决并发问题了。

idList.parallelStream().forEach(id -> {
    //对result变量加锁
    synchronized (result) {
        result.add(id);
    }
});

问题分析

问题解决了,但是为什么报的错误是数组越界异常呢?我们知道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也有自己不知道的问题。不过也正因为有这次踩坑经历,小卷对并发编程的了解又多了一些。

相关文章
|
安全
集合类在并发情况下如何保证线程安全
在正常单线程的情况下不会出现问题,当多线程的时候,List会出现 java.util.ConcurrentModificationException 这种异常
66 0
|
存储 安全 Java
【HashMap并发修改异常】
【HashMap并发修改异常】
311 0
|
安全
HashMap在多线程使用场景下会存在线程安全问题,怎么处理?
HashMap在多线程使用场景下会存在线程安全问题,怎么处理?
145 0
|
安全 开发者
JUC并发(并发的包)下的CopyOnWriteArrayList(线程安全的集合)
JUC并发(并发的包)下的CopyOnWriteArrayList(线程安全的集合)
97 0
JUC并发(并发的包)下的CopyOnWriteArrayList(线程安全的集合)
|
SQL 缓存 运维
一次 HashSet 所引起的并发问题(下)
首先了解下这个应用大概是做什么的。 简单来说就是从 MQ 中取出数据然后丢到后面的业务线程池中做具体的业务处理。 而报警的队列正好就是这个线程池的队列。
|
监控 Java 数据库
一次 HashSet 所引起的并发问题(上)
首先了解下这个应用大概是做什么的。 简单来说就是从 MQ 中取出数据然后丢到后面的业务线程池中做具体的业务处理。 而报警的队列正好就是这个线程池的队列。
|
安全
线程安全原理简析及HashMap多线程并发5种场景异常分析(2)
线程安全原理简析及HashMap多线程并发5种场景异常分析(2)
124 0
线程安全原理简析及HashMap多线程并发5种场景异常分析(2)
|
存储 算法 安全
线程安全原理简析及HashMap多线程并发5种场景异常分析(3)
线程安全原理简析及HashMap多线程并发5种场景异常分析(3)
248 0
线程安全原理简析及HashMap多线程并发5种场景异常分析(3)
|
安全 调度
线程安全原理简析及HashMap多线程并发5种场景异常分析(1)
线程安全原理简析及HashMap多线程并发5种场景异常分析(1)
207 0
线程安全原理简析及HashMap多线程并发5种场景异常分析(1)
|
Java 容器
为什么 HashMap 并发时会引起死循环?
今天研读Java并发容器和框架时,看到为什么要使用ConcurrentHashMap时,其中有一个原因是:线程不安全的HashMap, HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环。
为什么 HashMap 并发时会引起死循环?