java線上程序排錯經驗2 - 線程堆棧分析

1.前言

在線上的程序中,我們可能經常會碰到程序卡死或者執行很慢的情況,這時候我們希望知道是代碼哪裏的問題,我們或許迫切希望得到代碼運行到哪裏了,是哪一步很慢,是否是進入了死循環,或者是否哪一段代碼有問題導致程序很慢,或者出現了線程不安全的情況,或者是某些連接數或者打開文件數太多等問題,總之我們想知道程序卡在哪裏了,哪塊佔用了大量的資源。
此時,或許通過線程堆棧的分析就能定位出問題。
如果能深入掌握堆棧分析的技術,很多問題都能迎刃而解,但是線程堆棧分析並不簡單,設計到線上的排錯問題,需要有一定的知識的廣度,對某些知識也要有一定的深度,本人不才,無論是廣度還是深度,都感覺略有欠缺,工作兩年多來,碰到了很多問題,在解決問題時,並不是一帆風順的,其中可沒少折騰,但我相信,正確的理論可以指導實踐,本文也算是現學現賣。

注: 本文主要針對於Linux的,不過大多數在windows上也適用

2. 什麼是線程堆棧

線程棧
Java線程堆棧是某個時間對所有線程的一個快照,其中主要記錄瞭如下信息
– 線程的名稱
- 線程的ID
- 原生線程ID
- 線程的運行狀態
- 鎖的狀態
- 調用堆棧,也就是每個線程在各個方法調用的棧
上面描述的信息,接下來我們會具體的介紹與分析

3. 如何分析

首先我們得知道如何去得到線程堆棧
工具有很多,我這裏只介紹最原始的
先得到當前運行java程序的PID
這時候就要用到jps命令
jps常用的幾個命令

jps #顯示所有java程序和線程ID  
jps -m #輸出main method的參數 
jps -l #輸出完全的包名,應用主類名,jar的完全路徑名 
jps -v #輸出jvm參數

一般直接使用jps即可,如果分不清哪個pid是哪個程序,可以直接

jps -mlv

下面舉個簡單的線程堆棧分析的例子

package me.zks.jvm.troubleshoot.threadhump;
public class FirstSample {
    public static void main(String[] args) {
        FirstSample firstSample = new FirstSample();
        firstSample.fun1();
    }
    public void fun1(){
        //執行某些不耗時操作,然後直接進入fun2方法
        fun2();
    }
    public void fun2(){
        //執行某些不耗時的操作,然後直接進入fun3方法
        fun3();
    }
    public void fun3(){
        //一個死循環,或者耗時特別大的操作
        while (true){
            System.out.println("");
        }
    }
}

上面程序打成jar包,名爲jvm-troubleshoot-1.0-SNAPSHOT.jar,在linux上運行

java -cp jvm-troubleshoot-1.0-SNAPSHOT.jar me.zks.jvm.troubleshoot.threadhump.FirstSample

另外開一個session
輸入jps顯示如下

ubuntu@VM-0-12-ubuntu:~$ jps
27831 Jps
27819 FirstSample
ubuntu@VM-0-12-ubuntu:~$

輸入jps -mlv顯示如下

ubuntu@VM-0-12-ubuntu:~$ jps -mlv
27874 sun.tools.jps.Jps -mlv -Dapplication.home=/usr/lib/jvm/java-8-oracle -Xms8m
27819 me.zks.jvm.troubleshoot.threadhump.FirstSample
ubuntu@VM-0-12-ubuntu:~$

jstack 27819顯示如下

ubuntu@VM-0-12-ubuntu:~$ jstack 27819
2018-09-02 13:56:14
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode):

"Attach Listener" #8 daemon prio=9 os_prio=0 tid=0x00007fd350001000 nid=0x6d27 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fd3700c6800 nid=0x6cb3 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fd3700b1800 nid=0x6cb2 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fd3700af800 nid=0x6cb1 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fd3700ae000 nid=0x6cb0 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fd37007b000 nid=0x6caf in Object.wait() [0x00007fd35befd000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000ec6b2c98> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x00000000ec6b2c98> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:212)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fd370076800 nid=0x6cae in Object.wait() [0x00007fd35bffe000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000ec6b2e50> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x00000000ec6b2e50> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"main" #1 prio=5 os_prio=0 tid=0x00007fd37000a800 nid=0x6cac runnable [0x00007fd377121000]
   java.lang.Thread.State: RUNNABLE
        at java.io.FileOutputStream.writeBytes(Native Method)
        at java.io.FileOutputStream.write(FileOutputStream.java:326)
        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        - locked <0x00000000ec6bfe58> (a java.io.BufferedOutputStream)
        at java.io.PrintStream.write(PrintStream.java:482)
        - locked <0x00000000ec6b6ee0> (a java.io.PrintStream)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        - locked <0x00000000ec6b6e98> (a java.io.OutputStreamWriter)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.newLine(PrintStream.java:546)
        - eliminated <0x00000000ec6b6ee0> (a java.io.PrintStream)
        at java.io.PrintStream.println(PrintStream.java:807)
        - locked <0x00000000ec6b6ee0> (a java.io.PrintStream)
        at me.zks.jvm.troubleshoot.threadhump.FirstSample.fun3(FirstSample.java:22)
        at me.zks.jvm.troubleshoot.threadhump.FirstSample.fun2(FirstSample.java:17)
        at me.zks.jvm.troubleshoot.threadhump.FirstSample.fun1(FirstSample.java:13)
        at me.zks.jvm.troubleshoot.threadhump.FirstSample.main(FirstSample.java:9)

"VM Thread" os_prio=0 tid=0x00007fd37006f000 nid=0x6cad runnable

"VM Periodic Task Thread" os_prio=0 tid=0x00007fd3700cc000 nid=0x6cb4 waiting on condition

JNI global references: 5

通過上面的jstack我們可以指導,我們的這個簡單的程序,竟然有這麼多線程,分別是

"Attach Listener","Service Thread","C1 CompilerThread1","C2 CompilerThread0","Signal Dispatcher","Finalizer","Reference Handler","main","VM Thread","VM Periodic Task Thread",

其中,只有main線程是我們寫的,其他的線程是jvm啓動的時候,自己會創建一些輔助的線程,我們一般分析我們所寫的線程(也不一定,有時候也是需要分析其他線程的),我們來仔細分析一下我們得這個main線程,我給每一行標了個號

01 "main" #1 prio=5 os_prio=0 tid=0x00007fd37000a800 nid=0x6cac runnable [0x00007fd377121000]
02   java.lang.Thread.State: RUNNABLE
03        at java.io.FileOutputStream.writeBytes(Native Method)
04        at java.io.FileOutputStream.write(FileOutputStream.java:326)
05        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
06        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
07        - locked <0x00000000ec6bfe58> (a java.io.BufferedOutputStream)
08        at java.io.PrintStream.write(PrintStream.java:482)
09        - locked <0x00000000ec6b6ee0> (a java.io.PrintStream)
10        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
11        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
12        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
13        - locked <0x00000000ec6b6e98> (a java.io.OutputStreamWriter)
14        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
15        at java.io.PrintStream.newLine(PrintStream.java:546)
16        - eliminated <0x00000000ec6b6ee0> (a java.io.PrintStream)
17        at java.io.PrintStream.println(PrintStream.java:807)
18        - locked <0x00000000ec6b6ee0> (a java.io.PrintStream)
19        at me.zks.jvm.troubleshoot.threadhump.FirstSample.fun3(FirstSample.java:22)
20        at me.zks.jvm.troubleshoot.threadhump.FirstSample.fun2(FirstSample.java:17)
21        at me.zks.jvm.troubleshoot.threadhump.FirstSample.fun1(FirstSample.java:13)
22        at me.zks.jvm.troubleshoot.threadhump.FirstSample.main(FirstSample.java:9)

先分析第一行

"main" #1 prio=5 os_prio=0 tid=0x00007fd37000a800 nid=0x6cac runnable [0x00007fd377121000]
  • “main” 即爲線程的名稱,所以給線程去個好的名字很重要,方便於我們得分析
  • #1 我也不知道
  • prio=5 線程優先級,java 有1–10個優先級,當有多個線程處於可運行狀態時,運行系統總是挑選一個優先級最高的線程執行,兩個優先級相同的線程同時等待執行時,那麼運行系統會以round-robin的方式選擇一個線程執行,Java的優先級策略是搶佔式調度。
  • tid=0x00007fd37000a800 這個是java虛擬機內部的ID,與什麼操作系統沒關係,我們可以通過 java.lang.Thread.getId() 或者 java.lang.management.ThreadMXBean.getAllThreadIds()獲取到,在Oracle / Sun的JDK實現中,此ID只是一個自動遞增的long類型的變量。
  • nid=0x6cac linux或者windows等操作系統的線程ID,jvm作爲一個虛擬機,裏面的所有線程ID,其實都是映射到linux上面,這個ID就是linux機器實際運行的線程ID,這個很有用,我們可以通過這個來linux上查一些比較重要的信息,這個以後會有詳細說明。
  • runnable 線程的狀態,線程的狀態,這是從java虛擬機的角度來看的,表示線程正在運行,但是處於Runnable狀態的線程不一定真地消耗CPU,這個以後會說,線程狀態以後也會詳細說明。
    分析第二行
    java.lang.Thread.State: RUNNABLE, 描述了線程的狀態
    分析第3-22行。
    是方法的調用站的關係,我們一般從下往上看
    例如先看第22行
at me.zks.jvm.troubleshoot.threadhump.FirstSample.main(FirstSample.java:9)

這行顯示了正在調用的包名,類名,方法名稱,以及代碼中的第幾行
從上可知,main方法調用了fun1,fun1調用了fun2,fun2調用了fun3,fun3調用了java.io.PrintStream.println …..

使用top命令,看看是哪個線程佔用的cpu最多,一般使用top -Hp 27819
top -Hp 27819
top-hp pid!

線程鎖分析

代碼

jps

ubuntu@VM-0-12-ubuntu:~$ jps
21786 LockAnalysics
21804 Jps

jstack


ubuntu@VM-0-12-ubuntu:~$ jstack 21786
2018-09-02 22:06:31
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode):

"Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f0bac001000 nid=0x5563 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f0bd000a800 nid=0x551b waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"ThreadWaitTo" #10 prio=5 os_prio=0 tid=0x00007f0bd00ef800 nid=0x5526 waiting for monitor entry [0x00007f0bd4d17000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitTo.funWaitTo(LockAnalysics.java:76)
        - waiting to lock <0x00000000e2a78dc0> (a java.lang.Object)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitTo.run(LockAnalysics.java:70)
        at java.lang.Thread.run(Thread.java:748)

"ThreadWaitOn" #9 prio=5 os_prio=0 tid=0x00007f0bd00ee000 nid=0x5525 in Object.wait() [0x00007f0bd4e18000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e2a7eda8> (a java.lang.Object)
        at java.lang.Object.wait(Object.java:502)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitOn.funWaitOn(LockAnalysics.java:59)
        - locked <0x00000000e2a7eda8> (a java.lang.Object)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitOn.run(LockAnalysics.java:52)
        at java.lang.Thread.run(Thread.java:748)

"ThreadLocked" #8 prio=5 os_prio=0 tid=0x00007f0bd00e5800 nid=0x5524 waiting on condition [0x00007f0bd4f19000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadLocked.fun1(LockAnalysics.java:40)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadLocked.run(LockAnalysics.java:34)
        - locked <0x00000000e2a78dc0> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f0bd00be800 nid=0x5522 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f0bd00b1800 nid=0x5521 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f0bd00af800 nid=0x5520 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f0bd00ae000 nid=0x551f runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f0bd007b000 nid=0x551e in Object.wait() [0x00007f0bd56ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e2a08ed0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x00000000e2a08ed0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:212)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f0bd0076800 nid=0x551d in Object.wait() [0x00007f0bd5800000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e2a06bf8> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x00000000e2a06bf8> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=0 tid=0x00007f0bd006f000 nid=0x551c runnable

"VM Periodic Task Thread" os_prio=0 tid=0x00007f0bd00c4000 nid=0x5523 waiting on condition

JNI global references: 5

先看一下ThreadLocked線程, 線程狀態處於TIMED_WAITING(sleep),鎖特徵爲waiting on condition。由這個堆棧可知該線程搶佔了鎖0x00000000e2a78dc0

"ThreadLocked" #8 prio=5 os_prio=0 tid=0x00007f0bd00e5800 nid=0x5524 waiting on condition [0x00007f0bd4f19000] //!! 鎖特徵爲waiting on condition的
   java.lang.Thread.State: TIMED_WAITING (sleeping)  //線程狀態是TIMED_WAITING(sleeping)
        at java.lang.Thread.sleep(Native Method)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadLocked.fun1(LockAnalysics.java:40)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadLocked.run(LockAnalysics.java:34)
        - locked <0x00000000e2a78dc0> (a java.lang.Object)  // 該線程佔有了鎖0x00000000e2a78dc0, 進入了同步代碼塊的方法
        at java.lang.Thread.run(Thread.java:748)

再看一下ThreadWaitTo線程,可以發現線程狀態爲BLOCKED,鎖特徵爲waiting for monitor entry, waiting to lock <0x00000000e2a78dc0>說明鎖0x00000000e2a78dc0被其他線程佔用了,本線程正在等待搶佔這把鎖。

"ThreadWaitTo" #10 prio=5 os_prio=0 tid=0x00007f0bd00ef800 nid=0x5526 waiting for monitor entry [0x00007f0bd4d17000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitTo.funWaitTo(LockAnalysics.java:76)
        - waiting to lock <0x00000000e2a78dc0> (a java.lang.Object)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitTo.run(LockAnalysics.java:70)
        at java.lang.Thread.run(Thread.java:748)

看一下ThreadWaitOn線程, 線程處於WAITING狀態,鎖特徵爲Object.wait(),其中waiting on <0x00000000e2a7eda8>會釋放自己鎖定的鎖0x00000000e2a7eda8,其他線程可以佔有這把鎖,並且自己處於等待喚醒的狀態

"ThreadWaitOn" #9 prio=5 os_prio=0 tid=0x00007f0bd00ee000 nid=0x5525 in Object.wait() [0x00007f0bd4e18000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e2a7eda8> (a java.lang.Object) //此時wait方法會導致該鎖被釋放,其它線程又可以佔有該鎖
        at java.lang.Object.wait(Object.java:502)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitOn.funWaitOn(LockAnalysics.java:59)
        - locked <0x00000000e2a7eda8> (a java.lang.Object)
        at me.zks.jvm.troubleshoot.threadhump.LockAnalysics$ThreadWaitOn.run(LockAnalysics.java:52)
        at java.lang.Thread.run(Thread.java:748)

通過上面這個例子,我們看到了有locked,waiting to lock,waiting on這三個鎖特徵
- locked表示已經佔有了鎖
- waiting to lock表示這把鎖目前還沒搶到(可能別別的線程搶佔了),正在等待這把鎖
- waiting on 表示線程處於Object.lock()狀態,已經釋放了鎖,其他線程可以佔用這把鎖,同事本線程等待被喚醒.

3. 線程狀態的解讀

從上面的例子中,我們看到了線程狀態有BLOCKED,WAITING,TIMED_WAITING。 實際上線程狀態有如下幾種
線程狀態
1. 新建狀態(New):新創建了一個線程對象。
2. 就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
3. 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
4. 阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
(一)、WAITING (on object monitor) 等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
(二)、BLOCKED (on object monitor) 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。區分同步阻塞和等待阻塞也可以看鎖的特徵,例如同步阻塞鎖的特徵是waiting for monitor, 等待阻塞鎖的特徵是object.wait()
(三)、TIMED_WAITING(sleeping) 其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
5. 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

實際上我們jvm線程棧中,幾乎是不會出現NEW,RUNNING,DEAD 這些狀態,其中Runnable就算是正在運行了
處於WAITING, BLOCKED, TIME_WAITING的是不消耗CPU的,處於Runnable狀態的線程,是否消耗cpu要看具體的上下文情況
- 如果是純Java運算代碼, 則消耗CPU.
- 如果是網絡IO,很少消耗CPU,這點在分佈式程序中經常碰到
- 如果是本地代碼, 結合本地代碼的性質判斷(可以通過pstack/gstack獲取本地線程堆棧),
如果是純運算代碼, 則消耗CPU, 如果被掛起, 則不消耗CPU,如果是IO,則不怎麼消
耗CPU

4. 線程死鎖

死鎖
死鎖比較少見,而且難於調試:
所謂死鎖: 是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。
其實很久之前學習數字電路,經常會遇到一些鎖,這也是自動化的一些常見的問題,在計算機中,也有類似的東西,請看下圖
R1 和R2,都只能被一個進程使用
T1在使用R1,同時沒有使用完R1的情況下,想使用R2
T2在使用R2,同時在沒有使用完R2的情況下,想使用R1
這時,T1等待T2放棄使用R2,同時T2等待T1放棄使用R1,他們都不會放棄自己所使用的,於是產生了等待,將會一直僵持下去。
代碼如下

package me.zks.jvm.troubleshoot.threadhump;

public class DeadLock implements Runnable{
    private int flag = 1;
    // 兩個static的對象,靜態變量
    public static Object obj1=new Object();
    public static Object obj2=new Object();
    @Override
    public void run() {
        System.out.println("flag="+flag);
        if(flag==1){
            fun1();
        }
        if(flag==0){
            fun2();
        }
    }

    private void fun1() {
        synchronized (obj1){
            System.out.println("我已經鎖定了obj1,休息0.5秒後再鎖定obj2,但是估計進不了obj2,因爲obj2估計也被鎖定了");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj2){
                System.out.println("進入了obj2");
            }
        }
    }
    private void fun2() {
        synchronized (obj2){
            System.out.println("我已經鎖定了obj2,休息0.5秒後再鎖定obj1,但是估計進不了obj1,因爲obj1估計也被鎖定了");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj1){
                System.out.println("進入了obj1");
            }
        }
    }

    public static void main(String[] args) {
        DeadLock deadLock1 = new DeadLock();
        DeadLock deadLock2 = new DeadLock();
        deadLock1.flag=1;
        deadLock2.flag=0;
        System.out.println("線程開始了");
        new Thread(deadLock1).start();
        new Thread(deadLock2).start();

    }
}

運行後顯示

線程開始了
flag=1
我已經鎖定了obj1,休息0.5秒後再鎖定obj2,但是估計進不了obj2,因爲obj2估計也被鎖定了
flag=0
我已經鎖定了obj2,休息0.5秒後再鎖定obj1,但是估計進不了obj1,因爲obj1估計也被鎖定了

jstack顯示(刪除了一些信息,只留了用戶線程的)

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f37680062c8 (object 0x00000000e2a794f8, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f3768004e28 (object 0x00000000e2a79508, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at me.zks.jvm.troubleshoot.threadhump.DeadLock.fun2(DeadLock.java:41)
        - waiting to lock <0x00000000e2a794f8> (a java.lang.Object)
        - locked <0x00000000e2a79508> (a java.lang.Object)
        at me.zks.jvm.troubleshoot.threadhump.DeadLock.run(DeadLock.java:15)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at me.zks.jvm.troubleshoot.threadhump.DeadLock.fun1(DeadLock.java:28)
        - waiting to lock <0x00000000e2a79508> (a java.lang.Object)
        - locked <0x00000000e2a794f8> (a java.lang.Object)
        at me.zks.jvm.troubleshoot.threadhump.DeadLock.run(DeadLock.java:12)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

從線程中我們可以看到Found one Java-level deadlock:
這就說明進入了死鎖,分析Thread-1和Thread-2
Thread-1鎖定了0x00000000e2a79508, waiting to lock 0x00000000e2a794f8
Thread-0鎖定了0x00000000e2a794f8, waiting to lock 0x00000000e2a79508,於是這兩個線程就進入了死鎖。
如果通過這種方式發現了死鎖,那沒辦法,只有該代碼了,將併發做的更安全點纔是王道。

5. 大數據中死循環或者複雜度高的方法判斷

首先,如果是進入了死循環,程序肯定是在某個方法直接卡死,我們將線程堆棧分析下來,多分析幾次,比如20秒一次(具體情況而定),如果多次都在同一個方法棧的調用,但是根據我們得預估,這個代碼並不需要這麼多的時間,並且這個方法的線程佔用的cpu很高(前面提到的TOP -Hp pid, 然後根據這個方法的nid 轉成10進制就可以找到對應的線程),那麼我們就懷疑是這個cpu高引起的問題了。
這時候就需要分析代碼了。
需要注意的是,我們分析還是要根據代碼客觀的時間來分析,特別是在spark等大數據處理中,有的方法是需要很久的時間,合理的判斷,找出死循環或者複雜度高的方法,然後再對代碼進行修改。

6. IO或者網絡問題

有時候,大數據在shuffle過程中,或者web程序在傳輸數據過程中,可能會由於網絡等問題,導致程序會很慢,這個分析方法也同樣是通過看線程堆棧,查看到是java.io.,或者java.nio等問題,那就可能就是網絡問題了,網絡問題可以參考使用iotop 和iostat 來進行分析

7. 連接數瓶頸問題

有時候,linux機器的頻繁連接,同時連接數過多,或者IO的頻繁打開而不關閉,連接數過多,又或者是socketRead的併發太多,就容易發生連接數瓶頸問題。
最比較常見的就是在web做壓力測試(例如ab test),再併發數一多的時候,就容易發現瓶頸,像mysql有類似於MYTOP的工具,同樣,我們也可以通過jstack獲取方法棧來進行,例如下面這個例子
代碼參考自《java 問題定位技術》

"Thread-248" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable
[0xaeedb000..0xaeedc480]
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at oracle.net.ns.Packet.receive(Unknown Source)
... ...
at oracle.jdbc.driver.LongRawAccessor.getBytes()
"Thread-429" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable
[0xaeedb000..0xaeedc480]
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at oracle.net.ns.Packet.receive(Unknown Source)
... ...
at oracle.jdbc.driver.LongRawAccessor.getBytes()
"Thread-250" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable
[0xaeedb000..0xaeedc480]
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at oracle.net.ns.Packet.receive(Unknown Source)
... ...
at oracle.jdbc.driver.LongRawAccessor.getBytes()
.....

假如有太多的這類線程,那麼就可以得出是jdbc訪問過多,這時候就要優化資源和優化程序了。

7. 頻繁GC導致的cpu速度慢的問題

下回講解, 內存堆棧分析。

參考文獻:
HotSpotVM故障排除指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章