使用ParallelStream出现元素丢失、空指针异常、数组越界问题分析及解决方案

一.背景

        为了提高效率,使用parallelStream()并行流遍历list,过程中踩了一些坑,例如,数组元素丢失、数组越界、遍历时的空指针异常等,下面对这些问题进行了分析和记录。

二.问题

        先看看出问题的代码

    private List<String> getDesignDataByUrl(List<String> urlList){
        // MinIO数据获取:多线程并发处理
        List<String> finalDataList = new ArrayList<>();
        urlList.parallelStream()
                .filter(StringUtils::isNotEmpty)
                .forEach(url -> {
                    String jsonData = minIOUtils.getObjectContent(url);
                    finalDataList.add(jsonData);
                });
        return finalDataList;
    }

        可能出现的问题如下:

1.数组元素丢失
2.遍历时的空指针异常

        ArrayList是非线程安全的,上面是ArrayList增加元素的源码,其中add()方法中的elementData[size++] = e;是导致问题出现的根本原因,这行代码的执行过程可以拆解为:
        ①读取size值
        ②将e赋值到size的位置
        ③执行size++


        第三步size++也是非线程安全的,其执行过程在JVM中也分为三步:
        ①读取size的值(load)
        ②对size进行+1操作(add)
        ③将计算结果写回内存(store)


在多线程竞争时可能出现下面的场景:

        ①②线程A和线程B都读取到size=10
        ③④线程A给10赋值,并执行size++
        ⑤在size=11写回内存之前,线程B给10赋值
        ⑥线程A将size=11写入内存
        ⑦⑧线程B执行size++,此时size=12,并将size=12写入内存
        ⑨线程A继续执行,读取12,给12赋值,跳过了11,11为null

以上,size10被覆盖了两次,造成元素丢失;size11被跳过没有赋值,遍历时会产生空指针异常。

3.数组下标越界
        数组越界主要发生在扩容的临界点,数组容量的默认值是10 ,假设当前size为9,线程A和线程B都读取到9执行ensureCapacityInternal(size + 1),不会导致扩容,随后两个线程都执行elementData[size] = e,线程A的size++先完成并写回内存(此时size为10),线程B再执行size++,此时size值为11大于数组容量,就会造成数组越界。

三.解决方案

1.使用线程安全的ArrayList

List<String> finalDataList = new CopyOnWriteArrayList<>();

2.使用线程安全的LinkedList

ConcurrentLinkedQueue<String> finalDataList = new ConcurrentLinkedQueue();
LinkedBlockingQueue<String> finalDataList = new LinkedBlockingQueue();

3.将需要进行add操作的list,转换成线程安全的

List<String> finalDataList = Collections.synchronizedList(new ArrayList<>());

4.方案对比
        ①CopyOnWriteArrayList读操作高效,写操作开销大。由于写操作会创建一个新的数组副本,读操作不需要加锁,可以非常快速地完成,同时,大量的写操作会对内存和性能造成负担。因此,CopyOnWriteArrayList适合读多写少的场景。


        ②ConcurrentLinkedQueue使用非阻塞算法设计,同时,使用了CAS算法,在多线程环境下避免了锁的使用,从而减少了线程之间的竞争,但是由于其无界的特性,如果不加以控制可能会导致内存问题。
        LinkedBlockingQueue使用了内部锁(即synchronized)和条件变量来实现阻塞功能,因此在高并发环境下,其性能可能不ConcurrentLinkedQueue。但是,由于可以设置最大容量,能在一定程度上防止内存问题。并且,当队列满或空时,它可以阻塞线程,直到队列非满或非空为止,这对于需要同步的生产者-消费者模型非常有用。
        因此,ConcurrentLinkedQueue适用于需要高并发读写,且对内存使用有充足控制的场景。LinkedBlockingQueue适用于需要阻塞操作,且对队列大小有明确限制的场景。


        ③Collections.synchronizedList(new ArrayList<>())简单易用,但性能一般。对于只读操作,因为不需要同步,性能通常与普通列表类似。但对于写操作,因为需要同步,会有一定的性能开销。

至此,问题解决。

以上为个人观点,仅供学习记录,欢迎交流讨论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值