记一次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也有自己不知道的问题。不过也正因为有这次踩坑经历,小卷对并发编程的了解又多了一些。

相关文章
|
存储 Java 开发者
深入理解Jar文件:创建、使用和多版本控制
深入理解Jar文件:创建、使用和多版本控制
279 0
|
canal 缓存 NoSQL
面试官,如何保证缓存与数据库的数据一致性
面试官,如何保证缓存与数据库的数据一致性
|
12月前
|
Java 索引
Java“ArrayIndexOutOfBoundsException”解决
Java中的“ArrayIndexOutOfBoundsException”异常通常发生在尝试访问数组的无效索引时。解决方法包括:检查数组边界,确保索引值在有效范围内;使用循环时注意终止条件;对用户输入进行验证。通过这些措施可以有效避免该异常。
2251 2
|
9月前
|
人工智能 算法 搜索推荐
《开源算法:人工智能领域的双刃剑》
在人工智能蓬勃发展的今天,开源算法作为重要支撑,显著促进了算法创新、模型开发、技术进步与知识共享,并节省了时间与计算资源,降低了企业开发成本。然而,它也存在数据隐私与安全、个性化服务、创新速度、技术支持与维护及许可证与法律等方面的局限性。实际应用中需权衡优劣,选择合适方案以实现最大价值。
247 10
|
11月前
|
消息中间件 运维 UED
消息队列运维实战:攻克消息丢失、重复与积压难题
消息队列(MQ)作为分布式系统中的核心组件,承担着解耦、异步处理和流量削峰等功能。然而,在实际应用中,消息丢失、重复和积压等问题时有发生,严重影响系统的稳定性和数据的一致性。本文将深入探讨这些问题的成因及其解决方案,帮助您在运维过程中有效应对这些挑战。
234 1
|
11月前
|
缓存 监控 Java
Java 线程池在高并发场景下有哪些优势和潜在问题?
Java 线程池在高并发场景下有哪些优势和潜在问题?
211 2
|
设计模式 Java 调度
JUC线程池: ScheduledThreadPoolExecutor详解
`ScheduledThreadPoolExecutor`是Java标准库提供的一个强大的定时任务调度工具,它让并发编程中的任务调度变得简单而可靠。这个类的设计兼顾了灵活性与功能性,使其成为实现复杂定时任务逻辑的理想选择。不过,使用时仍需留意任务的执行时间以及系统的实际响应能力,以避免潜在的调度问题影响应用程序的行为。
179 1
|
算法 安全 Java
【经典算法】LeetCode 21:合并两个有序链表Java/C/Python3实现含注释说明,Easy)
【经典算法】LeetCode 21:合并两个有序链表Java/C/Python3实现含注释说明,Easy)
190 1
|
缓存 算法 Java
Java内存管理:优化性能和避免内存泄漏的关键技巧
综上所述,通过合适的数据结构选择、资源释放、对象复用、引用管理等技巧,可以优化Java程序的性能并避免内存泄漏问题。
244 5
|
前端开发 JavaScript Java
SpringBoot - 使用Spring Initializer 快速创建项目
SpringBoot - 使用Spring Initializer 快速创建项目
640 0