卡码笔记-最强八股文
首页
计算机基础
C++
Java
Go
大模型
  • Java面经
  • C++面经
  • 大模型面经
简历专栏
代码随想录 (opens new window)
首页
计算机基础
C++
Java
Go
大模型
  • Java面经
  • C++面经
  • 大模型面经
简历专栏
代码随想录 (opens new window)
  • 基础与面向对象

  • 集合

  • 异常

  • 字符串

  • JVM

  • 并发与多线程

    • Java创建线程有几种方式?
    • 线程start和run的区别?
    • Java线程安全的实现?
    • synchronized和lock、reentrantlock的区别是什么?
    • 说一说你对synchronized的理解?
    • volatile关键字的作用有哪些?
    • volatile与synchronized的对比?
    • 你知道Java中有哪些锁吗?
    • 为什么要有线程池,线程太多会怎样?
    • 说一说线程池的常用参数
    • BIO、NIO、AIO的区别
    • 什么是死锁?如何避免和排查
      • 简要回答
      • 详细回答
      • 代码示例
      • 排查步骤
      • 知识图解
      • 知识扩展
  • JDK

  • Spring

  • 设计模式

# 什么是死锁?如何避免和排查死锁?

# 简要回答

  • 死锁是指多个线程在竞争多个资源时,互相持有对方需要的资源,导致所有线程都无法继续执行的永久阻塞状态。
  • 发生死锁通常需要同时满足四个条件:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。
  • 避免死锁的核心思路是破坏这四个条件中的任意一个,工程上最常见的方法是:统一加锁顺序、减少嵌套锁、使用tryLock超时机制、不要在锁内执行耗时操作。
  • 排查死锁时可以通过jps + jstack、jcmd Thread.print -l、jconsole/arthas查看线程栈,重点关注BLOCKED线程、锁持有关系以及输出中的Found one Java-level deadlock。

# 详细回答

  1. 什么是死锁
    • 死锁本质上是循环等待。线程A持有资源1等待资源2,线程B持有资源2等待资源1,双方都不释放自己已经拿到的资源,最终谁也执行不下去。
    • 在Java里,死锁不仅可能发生在synchronized之间,也可能发生在ReentrantLock、数据库锁、分布式锁以及多种锁组合使用的场景中。
  2. 死锁产生的四个必要条件
    1. 互斥条件:同一时刻一个资源只能被一个线程占有。
    2. 持有并等待条件:线程已经持有一个资源,同时继续申请其他资源。
    3. 不可剥夺条件:线程已经拿到的资源,在自己释放前不能被强行抢走。
    4. 环路等待条件:多个线程之间形成首尾相接的资源等待环。
    • 只有这四个条件同时成立时,才会形成死锁,所以只要破坏其中任意一个条件,就能避免死锁。
  3. Java中常见的死锁场景
    • 加锁顺序不一致:线程A先锁A再锁B,线程B先锁B再锁A,是最常见的死锁原因。
    • 多资源混合加锁:本地锁、数据库行锁、分布式锁混用,如果获取顺序不统一,容易出现循环等待。
    • 锁内执行耗时操作:在线程持锁期间执行RPC、数据库查询、文件IO,会放大锁竞争时间,增加死锁概率。
    • 线程池任务互相等待:固定大小线程池中的任务互相Future.get()等待,也可能形成“线程池死锁”或饥饿型死锁。
  4. 如何避免死锁
    1. 统一资源申请顺序:这是最常用的方法。比如所有线程都先锁A再锁B,就不会形成环路等待。
    2. 一次性申请所有资源:如果某个资源拿不到,就释放已经获取的资源后重试,避免“持有并等待”。
    3. 使用可超时锁:ReentrantLock.tryLock(timeout)可以让线程在等待一段时间后放弃,避免永久阻塞。
    4. 使用可中断锁:lockInterruptibly()允许线程在等待锁时响应中断,适合复杂协作场景。
    5. 缩小锁粒度,减少嵌套锁:只锁真正需要同步的代码,尽量不要在一个锁里再套另一个锁。
    6. 不要在锁内做耗时操作:例如网络调用、长事务、远程接口、复杂计算等,都会显著增加死锁风险。
    7. 优先使用并发工具类:能用ConcurrentHashMap、原子类、阻塞队列解决的问题,就尽量不要手写多把锁配合。

# 代码示例

public class DeadLockDemo {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (LOCK_A) {
                sleep(100);
                synchronized (LOCK_B) {
                    System.out.println("t1 done");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (LOCK_B) {
                sleep(100);
                synchronized (LOCK_A) {
                    System.out.println("t2 done");
                }
            }
        }, "t2");

        t1.start();
        t2.start();
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  • 上面代码中,t1先拿LOCK_A再等LOCK_B,t2先拿LOCK_B再等LOCK_A,满足死锁四个条件后就会永久卡住。
  • 如果想避免这类问题,可以让所有线程都按照固定顺序获取锁,例如统一先拿LOCK_A再拿LOCK_B。

# 排查步骤

  1. 先看现象
    • 接口长时间无响应、线程一直不结束、CPU不一定很高,但业务明显“卡住”。
    • 线程状态中经常能看到多个线程长期处于BLOCKED或等待锁的状态。
  2. 定位Java进程
    • 使用jps -l查看Java进程ID。
  3. 导出线程栈
    • 使用jstack <pid>导出线程栈。
    • 或者使用jcmd <pid> Thread.print -l查看更详细的线程和锁信息。
  4. 重点看线程栈中的锁关系
    • 如果输出中直接出现Found one Java-level deadlock,说明JVM已经帮你识别出Java层面的死锁。
    • 继续看死锁线程名称、线程栈、等待的锁对象以及锁的持有者是谁。
    • 如果没有直接打印死锁,也要关注多个线程是否互相waiting to lock对方持有的对象。
  5. 结合代码和日志回溯
    • 根据线程名、类名、方法栈定位到具体业务代码。
    • 检查不同线程的加锁顺序是否一致,是否存在锁内调用数据库、RPC、MQ等耗时逻辑。
    • 如果涉及数据库事务,还要结合慢SQL日志、InnoDB死锁日志一起分析。
  6. 线上临时处理
    • 如果已经确认实例死锁且无法自行恢复,通常需要先重启实例或摘流处理,避免请求持续堆积。
    • 后续再通过线程栈、日志和代码复盘根因,不能只靠重启掩盖问题。

# 知识图解

  1. 死锁的出现 image

# 知识扩展

  1. 扩展:
  • jstack典型会打印出线程的等待关系,例如某个线程“waiting to lock”某个对象,而这个对象又被另一个线程持有。
  • 如果输出中出现Found one Java-level deadlock,说明JVM已经检测到Java层面的循环等待,这也是排查死锁时最直接的证据。
  • 在业务代码中还可以使用ThreadMXBean#findDeadlockedThreads()做定时探测,适合接入监控系统做告警。
  1. 面试官可能追问:
  • Q1:synchronized和ReentrantLock都可能发生死锁吗?
    • 都会。只要线程之间形成循环等待,就可能死锁。区别在于ReentrantLock支持tryLock()、超时和可中断等待,因此更容易做死锁预防。
  • Q2:死锁、活锁、饥饿有什么区别?
    • 死锁:线程彼此等待,完全不再向前推进。
    • 活锁:线程没有阻塞,一直在重试或让步,但是业务仍然没有进展。
    • 饥饿:某个线程长期抢不到资源,其他线程还能继续执行。
  • Q3:线程池也会发生死锁吗?
    • 会。例如固定大小线程池中,任务A等待任务B结果,任务B又等待任务A或者等待线程池中尚未执行的任务,就可能出现线程池内部互相等待的问题。
Last Updated: 4/30/2026, 11:54:17 AM

← BIO、NIO、AIO的区别 JDK8有什么新特性? →

评论

验证登录状态...

侧边栏
夜间
卡码简历
代码随想录
卡码投递表🔥
2026群
添加客服微信 PS:通过微信后,请发送姓名-学校-年级-2026实习/校招
支持卡码笔记
鼓励/支持/赞赏Carl
1. 如果感觉本站对你很有帮助,也可以请Carl喝杯奶茶,金额大小不重要,心意已经收下
2. 希望大家都能梦想成真,有好的前程,加油💪