死锁产生条件与诊断:jps、jstack、VisualVM
死锁题很容易被答成一句话:两个线程互相等待。
这句话当然对,但面试里不够。更完整的回答应该包括三层:
- 死锁怎么写出来的。
- 死锁成立需要哪些条件。
- 线上或本地怎么定位。
一个最典型的死锁
两个线程分别持有一把锁,又去等待对方手里的锁。
ObjectA=newObject();ObjectB=newObject();Threadt1=newThread(()->{synchronized(A){System.out.println("lock A");sleep(1000);synchronized(B){System.out.println("lock B");}}},"t1");Threadt2=newThread(()->{synchronized(B){System.out.println("lock B");sleep(500);synchronized(A){System.out.println("lock A");}}},"t2");t1.start();t2.start();执行到某个时刻:
线程t1持有锁 A,等待锁 B。
线程t2持有锁 B,等待锁 A。
谁都不肯放,谁都往下走不了。
死锁的四个必要条件
经典死锁有四个必要条件:
| 条件 | 含义 |
|---|---|
| 互斥条件 | 资源同一时刻只能被一个线程占有 |
| 占有且等待 | 线程持有资源的同时,还在等待其他资源 |
| 不可抢占 | 资源不能被外部强行剥夺,只能由持有者释放 |
| 循环等待 | 多个线程形成首尾相接的等待环 |
预防死锁,本质就是破坏其中一个条件。工程上最常见的是破坏循环等待:固定加锁顺序。
比如所有线程都先拿 A,再拿 B,就不会出现一个拿 A 等 B、另一个拿 B 等 A。
怎么用 jps 和 jstack 诊断
当 Java 程序疑似卡住,可以先用jps找进程。
jps-l找到目标进程 ID 后,用jstack打线程栈:
jstack-l<pid>如果确实发生死锁,jstack通常会在输出里给出类似信息:
Found one Java-level deadlock:并列出哪些线程持有哪些锁、正在等待哪些锁。
诊断链路可以这样看:
jstack的价值不只是告诉你“有死锁”。更关键的是告诉你线程卡在哪一行、等哪把锁、这把锁被谁持有。
可视化工具
本地开发或测试环境,也可以用 GUI 工具看线程。
| 工具 | 作用 |
|---|---|
jconsole | 监控 JVM 内存、线程、类加载等信息 |
| VisualVM | 查看线程、CPU、内存、堆栈、对象分配等 |
它们适合辅助定位,但线上排障不要只依赖 GUI。最通用、最容易落地的还是jps+jstack,因为服务器上通常能直接执行。
如何修复死锁
修复死锁不要只盯着“加大超时时间”。超时只能缓解,不能解释为什么锁顺序会错。
常见修复方式:
- 固定加锁顺序,所有线程都按同一顺序获取多把锁。
- 缩小锁粒度,减少一次持有多把锁的场景。
- 用
tryLock加超时,获取不到锁时主动释放已持有资源并重试。 - 避免在持锁期间调用外部服务、数据库、RPC 这类不可控耗时操作。
用tryLock的思想大概是:
if(lockA.tryLock(1,TimeUnit.SECONDS)){try{if(lockB.tryLock(1,TimeUnit.SECONDS)){try{// do something}finally{lockB.unlock();}}}finally{lockA.unlock();}}这不是鼓励所有地方都写复杂的tryLock,而是说明:当业务确实需要多把锁时,至少要设计失败退出路径。
面试怎么答
可以这样答:
死锁通常发生在多个线程需要同时获取多把锁时。比如线程 1 持有 A 锁等待 B 锁,线程 2 持有 B 锁等待 A 锁,就会互相等待,程序无法继续。
死锁有四个必要条件:互斥、占有且等待、不可抢占、循环等待。解决死锁就是破坏其中一个条件,项目里最常见的是固定加锁顺序,避免循环等待。
诊断时可以先用jps -l找 Java 进程,再用jstack -l pid查看线程栈。如果有 Java 级死锁,jstack会打印 deadlock 信息,并指出线程持有和等待的锁。也可以用jconsole、VisualVM 这类工具辅助查看线程状态。
