尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java 死锁排查实战:用 jstack 抓线程栈,定位互相等锁的 BLOCKED 环

Java 死锁排查实战:用 jstack 抓线程栈,定位互相等锁的 BLOCKED 环 Java 死锁排查实战:用 jstack 抓线程栈,定位互相等锁的 BLOCKED 环线上服务突然卡死:接口不返回、CPU 却很低、日志也不再滚动,重启能好一阵子又复发。这是典型的死锁特征——几个线程互相拿着对方要的锁,谁也不肯放,永远僵在那里。这篇手把手教你怎么用 jstack 把死锁揪出来,并讲清楚为什么会死、怎么改。先写一个必然死锁的例子死锁的经典条件:两个线程以相反的顺序获取两把锁。线程 1 拿了 A 等 B,线程 2 拿了 B 等 A,就锁死了。publicclassDeadlockDemo{privatestaticfinalObjectlockAnewObject();privatestaticfinalObjectlockBnewObject();publicstaticvoidmain(String[]args){// 线程 1:先 A 后 BnewThread(()-{synchronized(lockA){System.out.println(t1 got A);sleep(100);// 故意停一下,保证两个线程都拿到了第一把锁synchronized(lockB){// 等 lockB,但它被 t2 攥着System.out.println(t1 got B);}}},thread-1).start();// 线程 2:先 B 后 A,顺序和 t1 相反 → 埋下死锁newThread(()-{synchronized(lockB){System.out.println(t2 got B);sleep(100);synchronized(lockA){// 等 lockA,但它被 t1 攥着System.out.println(t2 got A);}}},thread-2).start();}privatestaticvoidsleep(longms){try{Thread.sleep(ms);}catch(InterruptedExceptionignored){}}}运行后你会看到打印停在t1 got A/t2 got B,然后程序既不结束也不报错,永远挂着。这就是死锁——没有任何异常,只是静静地卡住。第一步:找到卡住进程的 PIDjps-l# 输出类似:# 12345 DeadlockDemo# 12300 sun.tools.jps.Jpsjps是 JDK 自带的,列出所有 Java 进程。这里12345就是我们要查的进程。生产环境如果 jps 看不全(不同用户启动),用ps -ef | grep java也行。第二步:jstack 打印线程栈,自动检测死锁jstack12345stack.txtjstack会 dump 出所有线程的调用栈。最关键的是——如果存在死锁,jstack 会在输出末尾自动分析并明确标出来,不用你自己肉眼找。翻到最后能看到这样一段:Found one Java-level deadlock: thread-2: waiting to lock monitor 0x00007f... (object 0x000000076ab..., a java.lang.Object), which is held by thread-1 thread-1: waiting to lock monitor 0x00007f... (object 0x000000076ab..., a java.lang.Object), which is held by thread-2 Java stack information for the threads listed above: thread-2: at DeadlockDemo.lambda$main$1(DeadlockDemo.java:28) - waiting to lock 0x... (a java.lang.Object) ← 等这把锁 - locked 0x... (a java.lang.Object) ← 已经拿着这把 thread-1: at DeadlockDemo.lambda$main$0(DeadlockDemo.java:15) - waiting to lock 0x... - locked 0x... Found 1 deadlock.怎么读这段?核心看两个关键词:locked 地址:这个线程已经持有的锁。waiting to lock 地址:这个线程正在等的锁。把地址对起来看:thread-1 locked 的地址,正是 thread-2 waiting 的;反过来也成立。这就构成一个环:t1 等 t2 手里的锁,t2 等 t1 手里的锁。行号(DeadlockDemo.java:15/:28)直接告诉你死锁发生在哪行synchronized。没有现成 jstack?线程 dump 还有两条路生产容器里有时装不了完整 JDK,记住备用方案:# 方案一:jcmd 也能打线程栈,输出格式和 jstack 一致jcmd12345Thread.printstack.txt# 方案二:给进程发 SIGQUIT(kill -3),JVM 会把线程栈打到它的标准输出/日志里kill-312345kill -3不会杀进程,只是让 JVM 输出一次 thread dump,同样带死锁检测。容器里日志被收集时,这招特别有用。根治:破坏循环等待,统一加锁顺序死锁能成立要同时满足四个条件(互斥、持有并等待、不可抢占、循环等待)。工程上最容易打破的是循环等待——只要保证所有线程按同一个固定顺序获取多把锁,环就形成不了。// 修复:两个线程都按先 A 后 B的固定顺序,不再有相反顺序Runnabletask()-{synchronized(lockA){// 永远先拿 Asynchronized(lockB){// 再拿 B// 业务逻辑}}};如果锁对象是运行时才确定的(比如两个账户转账,锁的是账户对象),没法写死顺序,就用一个稳定的可比较字段(如账户 id)排序后再依次加锁:voidtransfer(Accountfrom,Accountto,longamount){// 用 id 大小决定加锁先后,保证任意两个账户的加锁顺序全局一致Accountfirstfrom.getId()to.getId()?from:to;Accountsecondfrom.getId()to.getId()?to:from;synchronized(first){synchronized(second){from.debit(amount);to.credit(amount);}}}还有一种思路是用ReentrantLock的tryLock(timeout)替代synchronized:拿不到第二把锁就超时放弃、释放已持有的锁、退避重试,从不可抢占这个条件上破解。但代价是逻辑更复杂、要处理重试,一般优先用统一加锁顺序。小结死锁的表现是卡死但 CPU 低、无异常无日志,和死循环(CPU 打满)是两码事。排查三板斧:jps找 PID →jstack piddump 线程栈 → 直接翻到末尾看Found one Java-level deadlock,JVM 已经帮你分析好了。读栈看两个词:locked(已持有)和waiting to lock(在等),地址对上就是死锁环,行号指向出事的synchronized。容器里没 jstack 就用jcmd Thread.print或kill -3,同样能拿到带死锁检测的 dump。根治靠破坏循环等待:全局统一加锁顺序;顺序不定时按对象 id 排序加锁;或用tryLock超时退避。一句话记忆点:多把锁一律按同一顺序获取,死锁的环就永远合不上。
返回列表