唯一稳妥做法是将rpc调用移出synchronized块:先同步更新本地状态,再异步触发rpc;须配超时、熔断、降级,并禁用finally中rpc,辅以静态检查工具拦截。

直接把 RPC 调用移出 synchronized 块,是唯一稳妥的做法。synchronized 本意是保护共享状态的原子性,不是兜底网络调用的“保险箱”——它既不能容忍超时、也无法隔离网络抖动,强行塞进去只会让锁变重、线程变卡、系统变脆。
拆开逻辑:先同步更新本地状态,再异步发RPC
把业务中“状态变更 + 外部通知”这类组合操作解耦。synchronized 块只做确定性动作:比如更新内存变量、修改数据库字段、设置标志位;RPC 调用单独拎出来,在锁外以异步方式触发。
- 例如:订单状态从“待支付”改为“已支付”,这个状态变更放 synchronized 块里;而通知风控系统、推送消息、写入日志等动作,全部移至锁外
- 推荐用 CompletableFuture 或线程池提交任务,避免阻塞主线程,也避免在 finally 中误用(见下文)
- 若需保证最终一致性,可将 RPC 参数暂存到本地队列(如 ConcurrentLinkedQueue)或发往轻量级本地消息(如 Disruptor),由后台消费者统一处理
加超时与降级:哪怕不在锁里,RPC 本身也要防护
移出去不等于放任不管。任何外部调用都必须自带熔断、超时、重试和 fallback —— 这些策略与 synchronized 无关,但关乎服务稳定性。
- 显式设置超时:Dubbo 配 consumer.timeout,Feign 配 connectTimeout / readTimeout,gRPC 用 withDeadlineAfter
- 失败不抛异常中断流程:用 orTimeout(2, SECONDS).exceptionally(e → log.warn("RPC failed, using default"))
- 关键路径做降级:比如风控校验失败时走默认白名单策略,而非直接报错回滚整个事务
警惕“伪解耦”:别在 finally 里补 RPC
有人以为“我把 RPC 放在 try-catch 的 finally 里,不就脱离锁了?”——这是更危险的陷阱。finally 仍运行在同一线程上下文中,且不具备超时控制能力,一旦 RPC 卡住,线程就被钉死,锁虽已释放,但线程无法复用,池子照样被耗尽。
- finally 只该做:关闭流、清 ThreadLocal、置 isDone = true 等毫秒级、无依赖、无IO的操作
- 所有对外通信(HTTP、Dubbo、MQ 发送)一律禁止出现在 finally 中
- 若需“无论如何都要上报”,应转为 oneway 发送(如 RocketMQ 的 sendOneway)或写入本地 ring buffer,由守护线程异步刷出
代码扫描与规范落地
靠人盯容易遗漏,建议接入静态检查工具主动拦截。
- 用自定义 SonarQube 规则或 SpotBugs 插件,检测 synchronized 块内是否含 HttpClient、DubboConsumer、RestTemplate 等敏感调用链
- 在 CI 流程中加入 checkstyle 或 Alibaba Java Coding Guidelines 插件,对 “synchronized.*\{.*\.post.*|\.invoke.*|\.send.*” 类正则做告警
- 团队约定:所有 RPC 调用必须带明确 timeout 注解(如 @RpcTimeout(ms = 1500)),未标注者编译失败
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











