使用CompletableFuture执行异步多线程任务时,使用ArrayList出现的越界问题

业务场景:

今天使用在做项目时,遇到了一个业务场景,其中要求如下:

  1. 远程调用基础数据服务获取教室树菜单列表
  2. 远程调用设备服务获取已绑定设备的教室列表
  3. 通过以上两个列表(树菜单、普通列表)进行处理,自己构造一个具有设备是否绑定标识的新树菜单(并预留拓展功能),这里涉及到递归操作,且需要创建对象
    如下图:
    界面

设计思路

因为其中涉及到了两个远程服务调用,一个树菜单重新构造(还涉及判断)以及递归操作,所以在这里我选择开启异步线程提高运行效率

代码实现

以下代码为实现类OnlineClassPatrolServiceImpl.java

    /**
     * 获取教室与设施关系树
     * 1、获取已绑定设备的教室ids
     * 2、获取当前机构的校区-教学楼-楼层-教室树
     * 3、构造新的教室与设备树
     *
     * @return
     * @throws BusException
     */
    @Override
    public R getClassroomEuqTree() throws BusException {
        Long orgId = UserUtil.getUserOrgId();
        // 获取已绑定教室ids
        List bindClassroomIds = equipmentFeign.getBindClassroomIds(2L, PlatformEnum.PLATFORM_RESOURCE.name());
        if (bindClassroomIds == null || bindClassroomIds.size() == 0) {
            return R.fail("当前机构未绑定任何设备");
        }
        // 根据机构获取校区-教学楼-楼层-教室树
        List selectTreeByJg = basicDataFeign.getSelectTreeByJg(2L);
        if (selectTreeByJg == null || selectTreeByJg.size() == 0) {
            return R.fail("当前机构下未设置教室");
        }
        // 定义教室与设备树以及树高度
        List classroomEquTreeVoList = new CopyOnWriteArrayList<>();
        // List> futures = new ArrayList<>(selectTreeByJg.size());
        // List classroomEquTreeVoList = Collections.synchronizedList(new ArrayList<>());
        List classroomEquTreeVoList = new ArrayList<>();
//        for (int i = 0; i < selectTreeByJg.size(); i++) {
//            int finalI = i;
//            futures.add(CompletableFuture.supplyAsync(() -> {
//                ClassroomEquTreeVo temp = new ClassroomEquTreeVo();
//                temp.setId(selectTreeByJg.get(finalI).getId());
//                temp.setLabelName(selectTreeByJg.get(finalI).getLabel());
//                temp.setLevel(0);
//                temp.setFlag(0);
//                if (selectTreeByJg.get(finalI).getChildren() != null && selectTreeByJg.get(finalI).getChildren().size() > 0) {
//                    temp.setChildList(this.getClassroomEquTreeVoList(selectTreeByJg.get(finalI).getChildren(), 1, bindClassroomIds));
//                }
//                return temp;
//            }, taskExecutor).whenComplete((res,e) -> {
//                if (e == null){
//                    classroomEquTreeVoList.add(finalI,res);
//                }
//            }));
//        }
        for (SelectTreeVo vo : selectTreeByJg){
            futures.add(CompletableFuture.supplyAsync(() -> {
                ClassroomEquTreeVo temp = new ClassroomEquTreeVo();
                temp.setId(vo.getId());
                temp.setLabelName(vo.getLabel());
                temp.setLevel(0);
                temp.setFlag(0);
                if (vo.getChildren() != null && vo.getChildren().size() > 0) {
                    temp.setChildList(this.getClassroomEquTreeVoList(vo.getChildren(), 1, bindClassroomIds));
                }
                return temp;
            }, taskExecutor));
        }
        // 主线程阻塞等待所有异步线程完成任务,并在join返回结果再添加到ArrayList,就不会出现线程安全问题
        futures.forEach(future -> classroomEquTreeVoList.add(future.join()));
        return R.suc(classroomEquTreeVoList);
    }

    /**
     * 递归获取子集
     *
     * @param selectTreeByJg   子集
     * @param level            层级
     * @param bindClassroomIds 已绑定设备的教室ids
     * @return
     */
    private List getClassroomEquTreeVoList(List selectTreeByJg, int level, List bindClassroomIds) {
        if (selectTreeByJg == null || selectTreeByJg.size() == 0) {
            return null;
        }
        List classroomEquTreeVoList = new ArrayList<>(selectTreeByJg.size());
        for (SelectTreeVo selectTreeVo : selectTreeByJg) {
            ClassroomEquTreeVo temp = new ClassroomEquTreeVo();
            temp.setId(selectTreeVo.getId());
            temp.setLabelName(selectTreeVo.getLabel());
            temp.setLevel(level);
            if (selectTreeVo.getChildren() != null && selectTreeVo.getChildren().size() > 0) {
                temp.setChildList(this.getClassroomEquTreeVoList(selectTreeVo.getChildren(), level + 1, bindClassroomIds));
            }
            if (level == 3) {
                // 如果是教室
                if (bindClassroomIds.contains(selectTreeVo.getId())) {
                    // 此教室已绑定设备
                    temp.setFlag(1);
                } else {
                    // 此教室未绑定设备
                    temp.setFlag(0);
                }
            } else {
                // 校区、教学楼、楼层
                temp.setFlag(0);
            }
            classroomEquTreeVoList.add(temp);
        }
        return classroomEquTreeVoList;
    }

可以看到,我注释的这一块一开始是这样的想法:

我开启异步线程的主要是在进行构造新的自定义树菜单时才开启的,再远程调用基础数据服务和调用设备服务时,我并没有进行异步操作,因为我觉得没必要。

以校区为遍历单位,各校区对的教室树菜单(远程基础数据调用获得)进行递归并判断,当为教室层级时,判断是否存在当前已绑定的教室列表(远程设备服务调用获得),若存在则设置自定义树菜单的flag字段为1(0为绑定,1已绑定)状态。

思路图如下所示:


image.png

发现问题

会出现北校区执行比较快的情况,则北校区的数据先放入List中,导致了最好的的结果resultList的顺序与一开始的基础数据的教室树菜单校区顺序不一致情况。
在这里我又不想对resultList结果列表进行重排序,所以我在每次异步任务执行完时,使用whenComplete将当前的List加入最后的classroomEquTreeVoList.add(finalI,res)中(finalI为下标索引,res为当前任务的子结果)

如果此时,北校区执行比较快,当他又是第二个执行,他先进入List,则出现List[1] = 北校区数据。现在下标应该为0的南校区还没进来,北校区由于执行得快,先进来了。

就像下面这个问题:index: 1,Size: 0


image.png
出现了线程不安全的问题,导致List classroomEquTreeVoList = 下标出现错误

解决方法(在这里我使用了第三种方法,阻塞主线程方法)

1、使用 CopyOnWriteArrayList (适合需要读多写少的业务场景)
  • List classroomEquTreeVoList = new CopyOnWriteArrayList<>();
2、使用 Collections.synchronizedList 将结果List设置为线程安全类
  • List classroomEquTreeVoList = Collections.synchronizedList(new ArrayList<>());
3、在主线程进行阻塞等待,当我们的所有异步任务(我这里是各校区完成结果递归)完成时,再将结果添加至新的List:
  • futures.forEach(future -> classroomEquTreeVoList.add(future.join()));

你可能感兴趣的:(使用CompletableFuture执行异步多线程任务时,使用ArrayList出现的越界问题)