一.背景
为了提高效率,使用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<>())简单易用,但性能一般。对于只读操作,因为不需要同步,性能通常与普通列表类似。但对于写操作,因为需要同步,会有一定的性能开销。
至此,问题解决。
以上为个人观点,仅供学习记录,欢迎交流讨论

2956

被折叠的 条评论
为什么被折叠?



