懵逼, controller 中两个接口调用 service 同一方法,方法中有递归操作,前端一个页面异步调用这两个接口,后端出现线程问题

347 天前
 Bryant0814

cantroller 层 @PostMapping("/traffic/records") public List listTrafficRecord() { List doorRecordInfoVOList = doorApiService.doorRecordList(null); return doorRecordInfoVOList.stream() .limit(3) .collect(Collectors.toList());

}

@GetMapping("/visitor/record") public List listVisitorRecord() { List doorRecordInfoVOList = doorApiService.doorRecordList("4"); return doorRecordInfoVOList.stream() .limit(2) .collect(Collectors.toList()); } service 实现

public List<DoorRecordInfoVO> doorRecordList(String typeFlag) {
    reentrantLock.lock();
    try {
        //首先获取 token
        String token = this.getToken();
        if (StrUtil.isEmpty(token)) {
            return null;
        }
        List<DoorRecordInfoVO> doorRecordInfoVOList = new ArrayList<>();
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime start = now.minusHours(12L);
        return handleList(doorRecordInfoVOList, now, start, token, typeFlag, 0);
    }finally {
        reentrantLock.unlock();
    }
}

private List<DoorRecordInfoVO> handleList(List<DoorRecordInfoVO> doorRecordInfoVOList,
                                          LocalDateTime now,
                                          LocalDateTime start,
                                          String token,
                                          String typeFlag,
                                          int retryCount) {
    //判断两个时间是不是在同一天
    if (now.toLocalDate().equals(start.toLocalDate())) {
        //在同一天组装时间范围
        String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String startTime = start.format(DateTimeFormatter.ofPattern("HHmmss"));
        String endTime = now.format(DateTimeFormatter.ofPattern("HHmmss"));
        JSONArray recordArray = this.apiResult(token, date, startTime, endTime);
        this.addArrayToList(recordArray, doorRecordInfoVOList, typeFlag);
    } else {
        //不在同一天
        String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String endTime = now.format(DateTimeFormatter.ofPattern("HHmmss"));
        JSONArray recordArray1 = this.apiResult(token, date, "000000", endTime);
        this.addArrayToList(recordArray1, doorRecordInfoVOList, typeFlag);
        //加上前一天时间段
        String preDate = start.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String startTime = start.format(DateTimeFormatter.ofPattern("HHmmss"));
        JSONArray recordArray2 = this.apiResult(token, preDate, startTime, "235959");
        this.addArrayToList(recordArray2, doorRecordInfoVOList, typeFlag);
    }
    if (doorRecordInfoVOList.size() < 3) {
        now = start;
        start = start.minusHours(1L);
        handleList(doorRecordInfoVOList, now, start, token, typeFlag, retryCount + 1);
    }
    return doorRecordInfoVOList;
}


private void addArrayToList(JSONArray array, List<DoorRecordInfoVO> doorRecordInfoVOList, String typeFlag) {
    if (ObjectUtil.isEmpty(array)) return;
    for (int i = 0; i < array.size(); i++) {
        JSONObject record = array.getJSONObject(i);
        DoorRecordInfoVO doorRecordInfoVO = new DoorRecordInfoVO();
        String type = record.getStr("recogResult");
        doorRecordInfoVO.setUserName(record.getStr("userName"));
        doorRecordInfoVO.setDoorName(record.getStr("doorName"));
        doorRecordInfoVO.setOpenTime(
                HcDateUtil.dateFormat(record.getStr("openTime"), "yyyyMMddHHmmss", "yyyy-MM-dd HH:mm:ss")
        );
        doorRecordInfoVO.setRecordType(type);
        if (StrUtil.isNotEmpty(typeFlag) && typeFlag.equals(type)) {
            doorRecordInfoVOList.add(doorRecordInfoVO);
        } else if (StrUtil.isEmpty(typeFlag)) {
            doorRecordInfoVOList.add(doorRecordInfoVO);
        }
    }
}

private JSONArray apiResult(String token, String date, String startTime, String endTime) {
    JSONArray jsonArray = null;
    //组装请求参数
    Map<String, Object> requestMap = new HashMap<>();
    Map<String, Object> map = new HashMap<>();

    map.put("beginTime", startTime);
    map.put("endTime", endTime);
    map.put("date", date);

    requestMap.put("data", map);
    //调用 api 获取数据
    String body = HttpRequest
            .post(getDoorRecordInfoUrl)
            .header("token", token)
            .header("appId", appId)
            .header("Content-Type", "application/json")
            .body(JSONUtil.toJsonStr(requestMap))
            .execute()
            .body();
    JSONObject resultJson = JSONUtil.parseObj(body);
    Integer code = resultJson.getInt("code");
    if (code == 200) {
        JSONObject data = resultJson.getJSONObject("data");
        jsonArray = data.getJSONArray("data");
    }
    return jsonArray;
}

上述实现如果我不加上 reentrantLock ,doorRecordInfoVOList 线程安全问题,有一个接口直接返回空了,但是直接分开调用,都有返回,请问这个为啥会有线程安全问题,求大佬指教

1755 次点击
所在节点    Java
13 条回复
chendy
347 天前
代码好乱
盲猜问题出在 this.getToken()
totoro52
347 天前
提供的代码中使用了 ReentrantLock 来同步对关键部分的访问。在 doorRecordList 方法内部对 handleList 方法的递归调用中存在潜在问题。在 doorRecordList 方法开始时获取了锁,并在 finally 块中释放了锁。如果递归调用 handleList 发生,可能会导致再次获取锁而不释放,导致死锁。
mango88
347 天前
检查下你的 this.getToken() 方法把
Bryant0814
347 天前
上述漏了一个 handleList 这个方法中最后那个判断是 doorRecordInfoVOList.size() < 3 && retryCount < 5 ,我的问题是,不加锁之前的话,调用这两个接口只有一个返回了数据,应该就是 doorRecordInfoVOList 出现了线程共享问题
Bryant0814
347 天前
@chendy 这个是直接调三方获取 token
@mango88
fordes
347 天前
后端出现线程问题可能是因为在 doorRecordList()服务方法中使用了全局的 ReentrantLock 对象( reentrantLock ),而在 Controller 层中有两个接口同时调用此服务方法。

当一个线程进入 doorRecordList()方法并获取锁之后,如果此时另一个线程也试图通过另一个接口调用该方法获取锁,由于锁已经被占用,第二个线程将会被阻塞,直到第一个线程执行完方法体内的逻辑并释放锁。在高并发场景下,如果前端页面异步同时调用这两个接口,可能会导致线程争抢锁资源,从而影响系统性能和响应速度。

另外,handleList() 方法内部递归调用自身时,每次递归都会尝试获取锁,这也会增加线程同步的复杂性。特别是在递归过程中涉及到网络 IO 操作(如 apiResult()方法中的 HTTP 请求),可能导致线程长时间持有锁而不释放,进而影响其他线程的执行。

为了解决这个问题,可以考虑以下策略:

确认是否需要在整个方法上加锁,根据业务需求看是否能将锁的作用范围缩小至实际有共享数据竞争的部分。
如果确实需要全局锁来保证数据一致性,可以考虑使用信号量或者条件变量等更细粒度的同步机制,以减少线程间的相互阻塞。
对于 HTTP 请求这类耗时操作,尽量避免在同步代码块或持有锁的情况下进行,可以通过异步处理、多线程池等方式提高并发能力。
总之,在设计并发访问的方法时,应确保正确合理地管理锁,避免造成不必要的线程阻塞,提高系统的并发处理能力和响应速度。


(通义千问的回答)
Bryant0814
347 天前
@fordes 我的问题是为什么,不加上 reentrantLock ,doorRecordInfoVOList 线程安全问题
vvtf
347 天前
@Bryant0814
#4 `doorRecordInfoVOList ` 不会有线程安全问题.
目前看下来只有 `getToken()`这里有问题才会出现你说的情况.
Bryant0814
347 天前
@totoro52 问题是不加上 reentrantLock ,doorRecordInfoVOList 线程安全问题
chendy
347 天前
@Bryant0814 emm…按照我能看懂的部分,你接口返空唯一的点是
String token = this.getToken();
if (StrUtil.isEmpty(token)) {
return null;
}
所以建议检查一下这里面有没有啥线程不安全的操作

顺便一说,线程不安全的前提之一是有被并发操作的成员变量,从你的代码上看不出来有这种情况

顺便 @Livid 楼上有个 AI 搬运工
sparklee
347 天前
返回空 是因为 getToken() empty 吗? doorRecordInfoVOList 没有多个线程共享吧, 应该不是这个问题
lsk569937453
347 天前
1.先把锁去了,这不是排查问题的方法。
2.handleList 里面我没看错的话是无限重试。在你的能力达不到的时候不要用递归。
3.后端能不重试就不重试,如果一个 http 请求出错了,你马上发一个请求,大概率也是出错的。
4.然后打日志看下 String token = this.getToken(); 这个返回值
Bryant0814
347 天前
已解决,是 token 问题,感谢各位大佬!!!

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1007497

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX