Wrayの知识库 Wrayの知识库
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
  • Java章节编写规范
  • Java基础

  • Java集合

  • Java并发

    • Java并发概述
    • 线程与进程
    • Thread类与线程生命周期
    • 线程创建与任务模型
    • 线程安全
    • synchronized关键字
    • volatile关键字
    • Java内存模型(JMM)
    • 线程间通信
    • 线程池
    • 并发工具类
    • 原子操作类Atomic
    • 并发锁
    • 并发容器
    • ConcurrentHashMap
    • BlockingQueue
    • CopyOnWriteArrayList
    • ThreadLocal
    • Fork/Join框架
    • ScheduledThreadPoolExecutor
    • CompletableFuture
    • 虚拟线程
    • 死锁活锁与线程问题排查
      • 1. 线程问题全景
      • 2. 死锁
      • 3. 活锁
      • 4. 饥饿
      • 5. 线程状态怎么看
      • 6. 线程 dump 排查方法
      • 7. CPU 飙高怎么定位到线程
      • 8. 锁竞争怎么优化
      • 9. 线上排查清单
      • 10. 面试表达
      • Tips 快问快答
    • 并发编程最佳实践
  • Java IO

  • JVM

目录

死锁活锁与线程问题排查

并发程序最难的地方不是写出多线程代码,而是在代码进入真实流量后,还能定位线程卡住、吞吐下降、CPU 飙高、请求超时和锁竞争等问题。

这一节重点讲清楚:

  • 死锁、活锁、饥饿、线程池饥饿分别是什么。
  • 线程问题为什么会导致服务假死。
  • 如何通过线程 dump 还原线程状态。
  • 线上遇到卡顿、CPU 飙高、锁等待时该怎么排查。

# 1. 线程问题全景

常见线程问题可以按“线程有没有继续做有效工作”来理解。

问题 现象 本质 常见原因
死锁 线程永久等待,业务卡住 互相持有对方需要的锁 多把锁加锁顺序不一致
活锁 线程一直运行但没有进展 反复让步、重试、状态互相干扰 过度礼让、无退避重试
饥饿 某些线程长期拿不到资源 调度或资源分配不公平 优先级不合理、锁竞争严重
线程池饥饿 任务排队不动或相互等待 工作线程被阻塞耗尽 同池任务互相 get/join
锁竞争 CPU 低但响应慢 大量线程等待同一把锁 锁粒度过大、热点数据集中
CPU 飙高 CPU 持续高位 线程忙循环或高频计算 死循环、频繁 GC、过度自旋

排查并发问题的核心思路是:先判断线程在运行、等待还是阻塞,再把线程状态和业务代码位置对应起来。

# 2. 死锁

死锁是指多个线程互相等待对方释放资源,导致所有相关线程都无法继续执行。

典型死锁:

线程 A
  持有 lock1
  等待 lock2

线程 B
  持有 lock2
  等待 lock1

示例:

Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        sleepQuietly(100);
        synchronized (lockB) {
            updateOrder();
        }
    }
}, "order-update-1");

Thread t2 = new Thread(() -> {
    synchronized (lockB) {
        sleepQuietly(100);
        synchronized (lockA) {
            updateOrder();
        }
    }
}, "order-update-2");

这段代码的问题是两个线程获取锁的顺序不一致。

死锁产生通常需要四个条件同时成立。

条件 含义 破坏方式
互斥 资源一次只能被一个线程持有 减少独占资源
持有并等待 持有一个锁时继续等待另一个锁 一次性申请资源或释放后再申请
不可抢占 资源不能被外部强制夺走 使用可超时锁
循环等待 多个线程形成等待环 固定加锁顺序

生产代码最常用的预防方式是固定锁顺序。

void transfer(Account from, Account to, long amount) {
    Account first = from.id() < to.id() ? from : to;
    Account second = from.id() < to.id() ? to : from;

    synchronized (first) {
        synchronized (second) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}

如果锁对象没有天然顺序,可以引入全局唯一序号,或者用额外的 tie lock 处理相同排序值。

# 3. 活锁

活锁不是线程阻塞,而是线程一直在运行,却因为互相改变策略导致没有实际进展。

示意:

线程 A 发现冲突 -> 让给 B -> 重新尝试
线程 B 发现冲突 -> 让给 A -> 重新尝试
线程 A 发现冲突 -> 让给 B -> 重新尝试
...

活锁常见于过度乐观重试:

while (!tryUpdate()) {
    Thread.yield();
}

这段代码的问题不是一定错误,而是没有退避、没有上限、没有降级策略。在高竞争下,所有线程可能不断失败再重试。

更稳妥的写法:

boolean updated = false;
for (int retry = 0; retry < 5; retry++) {
    if (tryUpdate()) {
        updated = true;
        break;
    }
    LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L * (retry + 1)));
}

if (!updated) {
    throw new ConcurrentUpdateException("更新竞争过高,请稍后重试");
}

活锁排查时,线程 dump 可能显示线程处于 RUNNABLE,但业务日志一直重复相同动作。

# 4. 饥饿

饥饿是指某些线程长期得不到 CPU、锁或队列执行机会。

常见原因:

  • 长任务占满线程池,短任务一直排队。
  • 非公平锁下某些线程长期竞争失败。
  • 单线程执行器中某个任务阻塞,后续任务全部无法执行。
  • 任务优先级队列中低优先级任务长期得不到调度。

线程池饥饿示例:

ExecutorService executor = Executors.newFixedThreadPool(2);

Future<String> orderFuture = executor.submit(() -> {
    Future<String> userFuture = executor.submit(this::queryUser);
    return userFuture.get();
});

Future<String> payFuture = executor.submit(() -> {
    Future<String> accountFuture = executor.submit(this::queryAccount);
    return accountFuture.get();
});

两个工作线程都在等待同一个线程池里的子任务,但子任务没有空闲线程执行,最终卡住。

修复思路:

  • 不要在同一个小线程池里提交子任务后同步等待。
  • 拆分不同类型线程池。
  • 使用异步编排,例如 CompletableFuture.thenCompose。
  • 为等待设置超时。
  • 限制队列长度并监控排队时间。

# 5. 线程状态怎么看

Java 线程 dump 中常见状态如下。

状态 含义 排查重点
NEW 已创建未启动 一般不是线上卡顿核心
RUNNABLE 正在运行或等待 CPU/系统调用 CPU 高、IO 调用、死循环
BLOCKED 等待进入 synchronized 锁竞争、死锁
WAITING 无限期等待其他线程唤醒 wait、join、park
TIMED_WAITING 带超时等待 sleep、定时等待、超时锁
TERMINATED 已结束 一般无需处理

状态转换示意:

NEW
 │ start
 ▼
RUNNABLE
 ├─ 竞争 synchronized 失败 -> BLOCKED
 ├─ wait/join/park       -> WAITING
 ├─ sleep/超时等待       -> TIMED_WAITING
 └─ run 结束             -> TERMINATED

注意:RUNNABLE 不一定正在消耗 CPU,也可能在等待操作系统层面的网络或磁盘 IO。

# 6. 线程 dump 排查方法

常用命令:

jstack -l <pid> > thread-dump.txt

或:

jcmd <pid> Thread.print -l > thread-dump.txt

建议连续采集 3 到 5 次,每次间隔 5 到 10 秒。

第 1 次 dump -> 看瞬时状态
第 2 次 dump -> 看是否停在同一位置
第 3 次 dump -> 判断是偶发等待还是持续卡住

单次 dump 只能说明那一刻发生了什么,连续 dump 才能看出趋势。

阅读线程 dump 时重点看:

  • 线程名称:是否能对应业务线程池。
  • 线程状态:RUNNABLE、BLOCKED、WAITING 的比例。
  • 栈顶方法:线程当前停在哪里。
  • 锁信息:locked、waiting to lock、parking to wait for。
  • 是否出现 JVM 自动识别的 deadlock。
  • 多个线程是否卡在同一个类、同一行代码。

典型 BLOCKED 片段:

"order-worker-17" #87 prio=5 os_prio=31 tid=0x... nid=0x...
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.example.OrderService.update(OrderService.java:88)
    - waiting to lock &lt;0x00000007012abcf0> (a java.lang.Object)

这说明线程正在等待进入某个 synchronized 保护的临界区。

# 7. CPU 飙高怎么定位到线程

排查流程:

top 找进程 PID
  │
  ▼
top -H 找高 CPU 线程 ID
  │
  ▼
线程 ID 转十六进制 nid
  │
  ▼
jstack 中搜索 nid
  │
  ▼
查看线程栈顶业务代码

Linux 示例:

top -H -p <pid>
printf "%x\n" <tid>
jstack -l <pid> > thread-dump.txt

如果高 CPU 线程栈顶反复停在同一段业务循环、正则、JSON 解析、加解密或集合遍历代码上,通常就是热点位置。

如果大量线程都在 GC 相关线程或 safepoint 附近,要结合 GC 日志和 JVM 章节继续排查。

# 8. 锁竞争怎么优化

锁竞争优化不是简单把 synchronized 换成 ReentrantLock。先确认竞争来自哪里。

优化方向 说明 示例
减小锁范围 锁住必要临界区 IO、RPC 不放进锁内
降低共享 拆分热点对象 分段统计、分账户锁
固定顺序 避免死锁 多账户转账按 ID 排序
读写分离 读多写少可用读写锁 配置快照读取
无锁替代 用原子类或不可变对象 状态标记、计数器
异步削峰 避免请求线程直接争抢 队列化处理

错误示例:

synchronized void refresh() {
    Config remote = configClient.load(); // 慢 IO
    this.config = remote;
}

修复示例:

void refresh() {
    Config remote = configClient.load();
    synchronized (this) {
        this.config = remote;
    }
}

锁只保护内存状态替换,远程调用放在锁外。

# 9. 线上排查清单

遇到服务卡顿或超时时,可以按下面顺序处理。

  1. 确认影响范围:单机、单接口、全站、某个下游。
  2. 保存现场:线程 dump、GC 日志、应用日志、CPU/内存/连接池指标。
  3. 看线程池:活跃线程、队列长度、拒绝次数、任务耗时。
  4. 看线程状态:是否大量 BLOCKED、WAITING、同栈 RUNNABLE。
  5. 看锁:是否出现 deadlock 或大量线程等待同一 monitor。
  6. 看外部依赖:数据库、缓存、HTTP 下游是否慢。
  7. 做止血:扩容、限流、熔断、重启、关闭问题入口。
  8. 做复盘:补超时、补监控、改锁顺序、拆线程池、加压测用例。

不要在没有保存现场的情况下反复重启。重启可能恢复服务,但也会抹掉最关键的证据。

# 10. 面试表达

如何排查 Java 死锁?

可以这样回答:

我会先用 jstack 或 jcmd Thread.print 采集线程 dump,并连续采集几次确认是否稳定卡住。然后看 JVM 是否直接报告 deadlock,再看线程状态中是否有大量 BLOCKED。重点关注 waiting to lock 和 locked 的对象,找到互相等待的锁链,回到业务代码检查多把锁的加锁顺序。修复时通常固定锁顺序、减少嵌套锁、使用可超时锁,必要时拆分共享资源。

如何排查 CPU 飙高?

可以这样回答:

我会先用 top 找到高 CPU 进程,再用 top -H -p pid 找具体线程,把线程 ID 转成十六进制后去线程 dump 中搜索 nid。如果多次 dump 中同一个线程都停在相同业务栈,说明那里可能有死循环、过度自旋或高频计算。如果高 CPU 与 GC 线程相关,还要结合 GC 日志、堆使用和对象分配速率排查。

# Tips 快问快答

Q:死锁和活锁有什么区别? A:死锁是线程互相等待不动,活锁是线程一直运行但没有实际进展。

Q:Java 能自动发现所有死锁吗? A:JVM 对对象监视器和部分 ownable synchronizer 死锁有识别能力,但业务层资源等待、数据库锁、分布式锁不一定能自动识别。

Q:线程 BLOCKED 一定是坏事吗? A:不一定。短时间锁竞争正常,长期大量 BLOCKED 才说明锁粒度、热点资源或死锁可能有问题。

Q:WAITING 是不是死锁? A:不是。WAITING 只是等待状态,可能是正常的队列消费者等待,也可能是任务互相等待,需要结合栈和持续时间判断。

Q:为什么要连续采集多次线程 dump? A:单次 dump 是瞬时快照,多次 dump 能判断线程是否一直卡在同一位置。

Q:线程池饥饿最常见的原因是什么? A:工作线程内又提交同池任务并同步等待,导致子任务没有线程执行。

Q:死锁怎么预防最有效? A:多把锁必须固定获取顺序,并避免在持有锁时调用外部慢操作。

Q:CPU 飙高时为什么要把线程 ID 转十六进制? A:线程 dump 中的 nid 通常用十六进制显示,需要转换后才能对应操作系统线程。

Q:锁竞争优化第一步是什么? A:先通过线程 dump、监控或 profiling 确认竞争点,再决定缩小锁范围、拆分热点或改用其他并发结构。

Q:线上卡顿能不能直接重启? A:可以作为止血手段,但重启前最好保存线程 dump、日志和监控现场,否则复盘会缺关键证据。

上次更新: 2026/06/25, 14:19:18
虚拟线程
并发编程最佳实践

← 虚拟线程 并发编程最佳实践→

Copyright © 2023-2026 Wray | 鄂ICP备2024050235号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式