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
相关产品推荐
相关产品推荐

