You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

synchronized线程中Thread.sleep未生效问题排查与代码优化

API测试写后读竞态问题排查与修复

问题背景

API测试场景下存在典型的写后读竞态问题:调用用户创建接口后需要校验对应用户是否存在,因此实现了最多4次重试的校验逻辑,该逻辑在本地运行正常,但在CI流水线环境中,代码内的Thread.sleep(1000)未按预期执行。

原有问题代码

存在性校验重试逻辑

public static Object commonExistenceQuery(Object endpoint) throws InterruptedException {
        int Counter = 0;
        Object ID = "";
        Object falseCheck = "false";

        synchronized (ID) {
            System.out.println("attempting to obtain Id for endpoint");
            ID = endpoint;
            while (ID == falseCheck) {
                Thread.sleep(1000);
                ID = endpoint;
                Counter++;
                System.out.println("Id not yet obtained, retrying attempt " + Counter + " of 4");
                if (Counter == 4|| ID != falseCheck) {
                    break;
                }
            }
            if (Counter == 4) {
                throw new ArithmeticException("ID was never obtained for endpoint, ensure successfully created");
            }
            else {
                System.out.println("successfully called endpoint and returned a valid ID of " + endpoint);
                return ID;
            }
        }
    }

用户查询逻辑

public Object findUser(Object name) throws IOException {

        JSONObject failure = new JSONObject();
        failure.put("check","false");

        Map request = new HashMap();
        Map body = new HashMap();

        body.put("name", name);
        body.put("business", "claims");
        body.put("unit", 10);
        request.put("body", body);
        request.put("callback", "true");

        Map findUser = run(userEndpoint.find, request,false);

        JSONObject jsonObject = new JSONObject(findUser);
        JSONObject getId = jsonObject.getJSONObject("data");
        if (getId == null){
            getId = failure;
            // we need to convert the null to a json object in the result of a failure, to avoid a null pointer exception
            System.out.println("failure to locate user ID, returning ID of "+getId.get("check"));
            return getId.get("check");
        }
        else
        {
            return getId.get("id");
        }
    }

Thread.sleep(1000)未生效的核心原因

观察到的“sleep未执行”本质是代码逻辑缺陷导致流程未走到符合预期的sleep分支,和流水线环境的线程调度无关,具体问题点如下:

  • 核心逻辑错误:重试时没有重新发起查询:Java中对象引用按值传递,commonExistenceQuery的入参endpoint是方法调用时传入的固定值,循环内反复执行ID = endpoint拿到的永远是第一次调用查询接口的结果,根本没有重新执行findUser方法发起新的查询。本地环境能跑通是因为本地服务响应快,第一次查询就拿到了有效ID,根本没进入循环触发sleep;流水线环境网络、服务负载更高,第一次查询大概率拿不到数据,进入循环后因为查询结果永远不会更新,就会出现“sleep没生效”的错觉。
  • 字符串比较逻辑错误:代码中用==比较字符串,比较的是两个对象的内存地址而非实际值。falseCheck是方法内定义的字符串常量,findUser返回的"false"是JSONObject解析出的字符串对象,二者内存地址大概率不相等,会直接导致while判断条件不成立,根本不会进入循环执行sleep。
  • 同步块完全无效:synchronized (ID)加锁的是方法内定义的局部空字符串,进入同步块后第一行就将ID重新赋值为endpoint,这个锁没有任何跨线程互斥作用,也不提供内存可见性保障,属于冗余代码。
  • 异常语义错误:查询失败抛出ArithmeticException属于语义完全不匹配的异常类型,会干扰问题排查。

可行优化方案

优化核心思路:重试逻辑必须在每次重试时重新发起接口查询,去掉无效冗余代码,修正基础语法错误,明确重试边界。

优化后的通用重试校验方法

import java.util.function.Supplier;

/**
 * 轮询校验资源是否存在,最多重试4次,每次间隔1秒
 * @param queryAction 实际查询逻辑,资源不存在时返回"false"
 * @return 有效资源ID
 * @throws InterruptedException 线程中断异常
 */
public static Object commonExistenceQuery(Supplier<Object> queryAction) throws InterruptedException {
    int maxRetry = 4;
    String falseFlag = "false";

    System.out.println("attempting to obtain Id for endpoint");
    for (int retryCount = 0; retryCount < maxRetry; retryCount++) {
        Object queryResult = queryAction.get();
        // 用equals比较字符串值,避免==地址比较的逻辑错误
        if (!falseFlag.equals(queryResult)) {
            System.out.println("successfully called endpoint and returned a valid ID of " + queryResult);
            return queryResult;
        }
        // 最后一次重试失败无需等待,直接进入异常分支
        if (retryCount < maxRetry - 1) {
            System.out.printf("Id not yet obtained, retrying attempt %d of %d%n", retryCount + 1, maxRetry);
            Thread.sleep(1000);
        }
    }
    throw new IllegalStateException("ID was never obtained for endpoint, ensure resource is successfully created");
}

优化后的用户查询方法

public Object findUser(String name) throws IOException {
    final String FAIL_FLAG = "false";
    Map<String, Object> request = new HashMap<>();
    Map<String, Object> body = new HashMap<>();

    body.put("name", name);
    body.put("business", "claims");
    body.put("unit", 10);
    request.put("body", body);
    request.put("callback", "true");

    Map<String, Object> findUserResult = run(userEndpoint.find, request, false);
    // 提前判空,避免空指针
    if (findUserResult == null) {
        System.out.println("failure to locate user ID, returning ID of " + FAIL_FLAG);
        return FAIL_FLAG;
    }
    JSONObject jsonObject = new JSONObject(findUserResult);
    // 用optJSONObject避免key不存在时抛出异常
    JSONObject data = jsonObject.optJSONObject("data");
    if (data == null || !data.has("id")) {
        System.out.println("failure to locate user ID, returning ID of " + FAIL_FLAG);
        return FAIL_FLAG;
    }
    return data.get("id");
}

调用方式

调用重试方法时传入查询动作本身,而非第一次查询的固定结果,保证每次重试都会发起新的接口请求:

// 传入lambda表达式作为查询动作,重试时会自动重新执行findUser方法
Object validUserId = commonExistenceQuery(() -> findUser(targetUserName));

优化点说明

  • 移除无效的synchronized块:重试逻辑是单线程顺序执行,对局部变量加锁没有任何实际作用
  • 每次重试重新执行查询逻辑:通过Supplier传入查询动作,真正实现“重试即重新请求”的效果
  • 统一用equals做字符串值比较,避免==地址比较导致的判断失效
  • 用for循环替代while循环,明确最大重试次数边界,避免死循环风险
  • 最后一次重试失败后直接抛出异常,不做无效sleep等待
  • 修正异常类型,用IllegalStateException表示业务状态不符合预期,语义匹配
  • 补全接口返回值的空判断逻辑,避免空指针导致流程意外中断
  • 给集合添加泛型约束,消除原始类型带来的类型安全风险

内容的提问来源于stack exchange,提问作者Jake

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 12:16:01