答案是排查非必要大锁导致的线程空转:识别长期BLOCKED线程,定位持有锁者执行过慢、锁粒度过粗(如静态变量或全局单例),结合线程dump、Arthas trace及jstat/top验证影响,优化为细粒度锁、移出耗时操作或改用无锁结构。

排查由并发竞争激烈的非必要大锁导致的空转开销,核心是识别那些本可避免却长期阻塞在 BLOCKED 状态的线程,并定位其背后不合理的大粒度锁设计。这类问题不会直接耗尽 CPU,但会显著降低吞吐、拉高响应延迟,且容易被误判为“CPU 不高所以不紧急”。
快速确认是否属于“非必要大锁”引发的 BLOCKED
先看线程 dump 中典型的可疑信号:
- 多个业务线程(如
http-nio-8080-exec-xx)状态均为BLOCKED (on object monitor),且都等待同一把锁(waiting to lock <0x...>) - 持有该锁的线程(
locked <0x...>)本身执行时间远超预期——比如在同步块内做了 I/O、远程调用、复杂计算或调用了慢方法 - 该锁对象是类级别静态变量、全局单例服务实例、或一个被过度共享的集合(如
static Map),而非真正需要跨线程强一致的细粒度资源 - 连续抓取 3–5 次 dump(间隔 5 秒),发现相同线程反复处于
BLOCKED,且等待队列长度持续增长
定位锁的来源与作用域
不能只看锁地址,要还原代码上下文:
- 在 jstack 输出中,找到
waiting to lock行上方最近的 Java 方法调用栈(通常是synchronized块或方法入口),确认锁对象的声明位置和生命周期 - 检查该锁是否被用于保护一段本可无锁操作的逻辑——例如:仅读取不可变配置、缓存命中后返回、或对局部变量的简单拼接
- 用 Arthas 的
trace命令跟踪锁所在方法的执行耗时:trace com.example.Service.doWork,观察实际耗时是否集中在非临界区 - 若使用
ReentrantLock,注意其堆栈中常出现Unsafe.park,此时需结合thread -b查看哪些线程在等同一把锁
验证空转开销的真实影响
BLOCKED 本身不消耗 CPU,但空转开销体现在调度与等待成本:
立即学习“Java免费学习笔记(深入)”;
- 用
jstat -gc <pid>观察 GC 频率是否异常升高——大量线程阻塞会导致任务堆积、队列膨胀、内存占用上升,间接触发频繁 Young GC - 用
top -H -p <pid>查看线程级 CPU,确认BLOCKED线程确实无 CPU 占用,而RUNNABLE线程(尤其是持有锁者)CPU 占比是否畸高 - 对比线程池活跃数与最大数:若活跃线程长期卡在低位(如 2/200),但请求积压严重,说明线程被锁“卡住”,未真正并发执行
针对性优化方向
目标不是消灭锁,而是消除“非必要”的大范围同步:
- 将全局锁拆分为键级锁(如用
ConcurrentHashMap.computeIfAbsent(key, k -> new ReentrantLock())),或改用StampedLock的乐观读 - 把锁内耗时操作(如 DB 查询、HTTP 调用)移出同步块,只在真正需要原子更新状态时加锁
- 用无锁结构替代:如读多写少场景下,用
CopyOnWriteArrayList或不可变集合;计数类用LongAdder - 评估是否可用
java.util.concurrent提供的线程安全类型(ConcurrentHashMap、ConcurrentLinkedQueue)直接替换手工同步的HashMap+synchronized



















