问题解决记录
问题背景:
测试人员反馈某银行系统 “商户费率审核”菜单的“批量审核通过”功能,每次对列表数据批量审核时,总会成功审核一部分,遗留一部分。
问题发现:
经排查,发现以下代码:
public void function(){
...
List<MerchantRateTmp> list;
List<String> listM = new ArrayList<>();
if (rowTotal > 0) {
list = merchantRateService.getMerchantRateTmpByPage(map);
list.parallelStream().forEach(v -> {
listM.add(v.getMchntNo());
});
}
...
}
对list 列表进行并行流遍历的时候,给一个ArrayList类型的listM列表新增值,再用listM的结果(校验真实性,去重后要审核的商户编号列表)去做审核操作。但每次对相同的商户费率进行审核,listM的结果都不相同,而且listM列表中会出现很多null 值。
问题解析:
- parallelStream 是一种并行流,它利用多线程来加速集合的遍历和操作。
- ArrayList 是一个非线程安全的集合类。(HashMap、HashSet同理)
- 当使用 parallelStream 对 ArrayList 进行操作(添加、删除、修改)时,就会引发线程安全问题。如下:
(1) 数据不一致:多个线程同时修改集合,可能导致数据丢失或覆盖,丢失解释了为什么listM列表中会出现很多null 值,覆盖解释了为什么会批量审核时没有全部审核。
(2) 并发修改异常:在遍历过程中增删集合元素,可能会抛出 ConcurrentModificationException。
问题解决
-
将ArrayList 替换为线程安全的集合类,如 CopyOnWriteArrayList。CopyOnWriteArrayList 通过在写操作时创建副本来保证线程安全。
-
使用 map、filter 等流操作生成新的集合,而不是新建集合赋值。
list.stream().map(MerchantRateTmp::getMchntNo).collect(Collectors.toList()); -
使用 Collections.synchronizedList将 ArrayList 包装为线程安全的集合。
List<String> listM = Collections.synchronizedList(new ArrayList<>()); -
数据量少,放弃parallelStream 并行,直接串行。
list.forEach(v -> listM.add(v.getMchntNo())); -
…
问题探索:
举例ArrayList 的 add(E e) 方法:
/**
* Appends the specified element to the end of this list.
* 将指定的元素追加到列表的末尾。
* add() 方法做了如下操作:
* 1.检查容量是否足够,如不够将进行扩容,并自增 modCount
* 2.将指定的元素追加到列表的末尾
*
* @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!!
//将e存在index为size的位置(即最后一位的下一位置),size++
//我们都知道,++操作不是原子指令,多线程情况下将发生并发问题
elementData[size++] = e;
return true;
}
[!TIP]
其中 elementData[size++] = e; 这句代码可以分开为
1.elementData[size] = e;
2.size = size + 1;
当多个线程同时执行以上代码就可能出现:
| 执行操作 | 线程一 | 线程二 | 当前步骤结果 | 正确预期 |
|---|---|---|---|---|
| 往elementData空列表中添加一个值 | 获取size 为0,不用扩容 | 获取size 为0,不用扩容 | size = 0, elementData[0]=null, elementData[1]=null | size = 0, elementData[0]=null, elementData[1]=null |
| elementData[size] = e; | 往elementData[0]中添加值A | size = 0, elementData[0]=A, elementData[1]=null | size = 0, elementData[0]=A, elementData[1]=null | |
| 往elementData[0]中添加值B | size = 0, elementData[0]=B, elementData[1]=null | size = 0, elementData[0]=A, elementData[1]=B | ||
| size = size + 1; | size+1 | size = 1, elementData[0]=B, elementData[1]=null | size = 1, elementData[0]=A, elementData[1]=B | |
| size+1 | size = 2, elementData[0]=B, elementData[1]=null | size = 2, elementData[0]=A, elementData[1]=B |
详细介绍参见
1.https://blog.csdn.net/u012859681/article/details/78206494
2.https://blog.csdn.net/weixin_36378917/article/details/81812210
2380

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



