同步执行阻塞源于线程等待资源时被挂起且调用栈滞留,预防关键在于从源头控制同步范围、时长与依赖:精简临界区、移出非共享操作、禁I/O与睡眠、设锁超时、统一多锁顺序、以异步替代同步等待,并主动监控阻塞点。

限制同步临界区的粒度和时长
过长的同步块会放大阻塞风险——哪怕只是一段日志记录或简单计算,若裹在锁内,也会拖慢所有等待线程。
- 只对真正需要互斥访问的共享状态加锁,把非共享操作(如参数校验、本地变量计算、JSON序列化)移出 synchronized 块或 Lock 作用域
- 避免在同步块内做 I/O(文件读写、HTTP 调用、数据库查询)、睡眠(Thread.sleep)、或调用未知行为的第三方方法
- 为锁操作设置超时:ReentrantLock.tryLock(timeout, unit) 比无条件 lock() 更安全,失败后可降级或重试,而非无限等待
统一锁获取顺序,切断死锁链
多个资源同时加锁时,顺序混乱是死锁的直接诱因;而死锁会让调用栈卡在 wait() 或 park() 状态,深度不变但线程永久阻塞。
- 对所有需多锁操作的对象,定义全局唯一排序依据(如 Account.id 的自然序、对象 hashCode() 的绝对值、或预分配的 long 序号)
- 每次按升序(或降序)依次尝试获取锁,确保所有线程遵循同一路径,消除循环等待条件
- 示例:转账前先锁 id 小的账户,再锁 id 大的,无论参数传入顺序如何
用异步/非阻塞替代同步等待
当阻塞不可避免(如远程调用),就该让等待不占用业务线程——把“同步等结果”变成“异步收通知”,释放调用栈上下文。
- 对 HTTP、RPC、DB 查询等 I/O 密集操作,优先使用 CompletableFuture、WebClient、或响应式客户端(如 R2DBC),配合线程池隔离
- 定时任务、消息消费等场景,用 @Async + 自定义线程池,避免挤占 Tomcat 或 Netty 的 I/O 线程
- 前端调用后端接口时,避免服务端同步等待下游响应;改用回调、事件总线或状态轮询机制
主动暴露阻塞点,便于快速定位
调用栈深度本身不是问题,但长期停留在某一层(如 Object.wait、Unsafe.park、SocketInputStream.read)就是明确信号。
- 启用 JVM 线程 dump 定时采集(如每 5 分钟 jstack),结合 APM 工具(如 SkyWalking、Arthas)标记阻塞线程及其锁持有者
- 在关键同步入口打日志,记录进入时间、锁对象标识、预计最大等待时长;超时未退出则告警
- 对数据库操作,开启 slow query 日志 + 锁等待监控(如 MySQL 的 performance_schema.data_locks)

















