ArrayList 线程不安全导致的bug

问题解决记录

问题背景:

测试人员反馈某银行系统 “商户费率审核”菜单的“批量审核通过”功能,每次对列表数据批量审核时,总会成功审核一部分,遗留一部分。

问题发现:

经排查,发现以下代码:

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 值。

问题解析:

  1. parallelStream 是一种并行流,它利用多线程来加速集合的遍历和操作。
  2. ArrayList 是一个非线程安全的集合类。(HashMap、HashSet同理)
  3. 当使用 parallelStream 对 ArrayList 进行操作(添加、删除、修改)时,就会引发线程安全问题。如下:
    (1) 数据不一致:多个线程同时修改集合,可能导致数据丢失或覆盖,丢失解释了为什么listM列表中会出现很多null 值,覆盖解释了为什么会批量审核时没有全部审核。
    (2) 并发修改异常:在遍历过程中增删集合元素,可能会抛出 ConcurrentModificationException。

问题解决

  1. 将ArrayList 替换为线程安全的集合类,如 CopyOnWriteArrayList。CopyOnWriteArrayList 通过在写操作时创建副本来保证线程安全。

  2. 使用 map、filter 等流操作生成新的集合,而不是新建集合赋值。

    list.stream().map(MerchantRateTmp::getMchntNo).collect(Collectors.toList());
    
  3. 使用 Collections.synchronizedList将 ArrayList 包装为线程安全的集合。

    List<String> listM = Collections.synchronizedList(new ArrayList<>());
    
  4. 数据量少,放弃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]中添加值Asize = 0,
elementData[0]=A,
elementData[1]=null
size = 0,
elementData[0]=A,
elementData[1]=null
往elementData[0]中添加值Bsize = 0,
elementData[0]=B,
elementData[1]=null
size = 0,
elementData[0]=A,
elementData[1]=B
size = size + 1;size+1size = 1,
elementData[0]=B,
elementData[1]=null
size = 1,
elementData[0]=A,
elementData[1]=B
size+1size = 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值