同步块内严禁调用耗时方法,应仅执行最小原子操作;耗时逻辑须移出锁外,采用“先复制后处理”模式,并优先使用细粒度锁或并发工具类替代粗粒度同步。

在 Java 中,同步块(synchronized)内调用外部耗时方法,会显著延长锁持有时间,导致线程阻塞加剧、吞吐量下降,甚至引发死锁或响应延迟。核心原则是:**只在同步块中做真正需要互斥保护的最小原子操作,把耗时、I/O、网络、锁无关逻辑全部移出同步范围。**
识别并剥离耗时操作
常见耗时操作包括:数据库查询、HTTP 调用、文件读写、序列化/反序列化、复杂计算、日志打印(尤其带堆栈或格式化)、调用第三方 SDK 方法等。这些都不该出现在 synchronized 块内。
- 错误示例:
synchronized(obj) {<br> int result = db.query("SELECT count(*) FROM users"); // 耗时 I/O<br> log.info("Count: {}", result); // 可能阻塞(异步日志器未配置好时)<br> updateCache(result); // 外部服务调用<br>} - 正确做法:先获取必要数据(如状态、ID、计数器值),再释放锁,最后执行耗时动作。
int count;<br>synchronized(obj) {<br> count = cachedUserCount; // 仅读一个 volatile 或受保护字段<br>}<br>// 锁已释放<br>db.queryAsync("SELECT count(*) FROM users").thenAccept(c -> updateCache(c));<br>log.debug("Fetched count: {}", count);
用“先复制后处理”模式替代“边锁边调用”
当需要基于共享状态做后续操作时,不要在锁中直接调用外部方法,而是把关键中间结果(如对象引用、数值、布尔标志)安全地拷贝出来,再在锁外使用。
- 若需修改后更新远程服务,可先在同步块中生成待提交的数据快照(如
new OrderSnapshot(orderId, status, version)),再解锁后调用orderService.submit(snapshot) - 对集合类操作,避免在同步块中遍历并逐个调用
httpClient.send();改为同步块内构建请求参数列表(List<req></req>),再循环发送 - 注意深拷贝问题:若拷贝的是可变对象(如
ArrayList),需确保其内容不被其他线程并发修改,必要时用不可变容器(ImmutableList)或防御性复制
用更细粒度的锁或无锁结构替代粗粒度同步
有时问题根源不是“怎么移出耗时方法”,而是“为什么需要用大同步块”。考虑以下替代方案:
- 将单一锁拆分为多个独立锁(如按 key 分段加锁:
locks[key.hashCode() & 0x1f].lock()) - 用
java.util.concurrent中的线程安全类替代手动同步:如ConcurrentHashMap、AtomicInteger、StampedLock(支持乐观读)、CopyOnWriteArrayList(适合读多写少) - 对纯状态变更场景,尝试用 CAS 操作(
compareAndSet)+ 循环重试,避免阻塞式锁 - 业务上允许短暂不一致?可改用最终一致性模型,用消息队列异步刷新下游,完全消除同步块
静态检查与监控辅助识别风险
光靠人工容易遗漏。建议结合工具提前拦截:
- 使用
SpotBugs(原 FindBugs)规则DL_DELEGATING_NULL_CHECK和自定义规则检测synchronized块内是否含.execute()、.query()、.send()等可疑调用 - 在测试环境开启 JVM 参数
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMethods配合 JFR(Java Flight Recorder),录制高锁竞争场景,定位同步块执行耗时分布 - 在关键同步方法入口添加
System.nanoTime()打点,记录锁内执行时间,告警超过阈值(如 >5ms)的调用
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











