1.如何獲取線程 dump (java-core)文件
#1.jstack
jstack -l <pid> >> <file-path>
如: jstack -l 37320 > /opt/tmp/threadDump.txt
#2.kill -3
kill -3 <pid>
#3.JVisualVM圖形工具采集
注意:
>> 在實際運行中,往往一次 dump的信息,還不足以確認問題。
建議多次 dump,尋找其中的共性與不同點。
分析工具參考: http://www.itdecent.cn/p/200416bc3964
2.線程分析
2.1 線程狀態(tài)分析
2.1.1 Runnable
該狀態(tài)表示線程具備所有運行條件,在運行隊列中準備操作系統(tǒng)的調度,或者正在運行。
一般情況下處于運行狀態(tài)線程是會消耗CPU的,但不是所有的RUNNABLE都會消耗CPU,
比如線程進行網絡IO時,這時線程狀態(tài)是掛起的,但由于掛起發(fā)生在本地代碼,虛擬機并不感知,
所以不會像顯示調用Java的sleep()或者wait()等方法進入WAITING狀態(tài),只有等數據到來時才消耗一點CPU.
2.1.2 BLOCKED
此時的線程處于阻塞狀態(tài),一般是在等待進入一個臨界區(qū)“waiting for monitor entry”,這種狀態(tài)是需要重點關注的.
###Waiting for monitor entry
在多線程的 JAVA程序中,實現線程之間的同步,就要說說Monitor。
Monitor是 Java中用以實現線程之間的互斥與協作的主要手段,它可以看成是對象或者 Class的鎖。
每一個對象都有,也僅有一個 monitor。
每個 Monitor在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,
而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set”和 “Wait Set”里面等候。
在 “Entry Set”中等待的線程狀態(tài)是 “Waiting for monitor entry”,
而在 “Wait Set”中等待的線程狀態(tài)是 “in Object.wait()”。
先看 “Entry Set”里面的線程。我們稱被 synchronized保護起來的代碼段為臨界區(qū)。
當一個線程申請進入臨界區(qū)時,它就進入了 “Entry Set”隊列。對應的 code就像:
synchronized(obj) {
.........
}
這時有兩種可能性:
>> 該 monitor 不被其它線程擁有, Entry Set里面也沒有其它等待線程。
本線程即成為相應類或者對象的 Monitor的 Owner,執(zhí)行臨界區(qū)的代碼. 此時為Runnable.
>> 該 monitor 被其它線程擁有,本線程在 Entry Set隊列中等待。 此時為BLOCKED.
#BLOCKED狀態(tài)下, 對應的堆棧示例:
"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8]
at testthread.WaitThread.run(WaitThread.java:39)
- waiting to lock <0xef63bf08> (a java.lang.Object)
- locked <0xef63beb8> (a java.util.ArrayList)
at java.lang.Thread.run(Thread.java:595)
臨界區(qū)(synchronized)的設置,是為了保證其內部的代碼執(zhí)行的原子性和完整性。
但是因為臨界區(qū)在任何時間只允許線程串行通過,這 和我們多線程的程序的初衷是相反的。
如果在多線程的程序中,大量使用 synchronized,或者不適當的使用了它,
會造成大量線程在臨界區(qū)的入口等待,造成系統(tǒng)的性能大幅下降。
如果在線程 DUMP中發(fā)現了這個情況,應該審查源碼,改進程序。
2.1.3 TIMED_WAITING/WATING
表示線程被掛起,必須等待notify()或notifyAll()或unpark()或接收到interrupt信號才能退出等待狀態(tài).
>> 當設置超時時間時狀態(tài)為TIMED_WAITING;
>> 如果是未設置超時時間,這時的狀態(tài)為WATING.
#TIMED_WAITING/WATING下還需要關注下面幾個線程狀態(tài):
###1.waiting on condition:
說明線程等待另一個條件的發(fā)生,來把自己喚醒, 該狀態(tài)出現在線程等待某個條件的發(fā)生。
具體是什么原因,可以結合 stacktrace來分析。
最常見的情況是線程在等待網絡的讀寫,比如當網絡數據沒有準備好讀時,線程處于這種等待狀態(tài),
而一旦有數據準備好讀之后,線程會重新激活,讀取并處理數據。
在 Java引入 NIO之前,對于每個網絡連接,都有一個對應的線程來處理網絡的讀寫操作,
即使沒有可讀寫的數據,線程仍然阻塞在讀寫操作上,這樣有可能造成資源浪費,而且給操作系統(tǒng)的線程調度也帶來壓力。
在 NIO里采用了新的機制,編寫的服務器程序的性能和可擴展性都得到提高。
如果發(fā)現有大量的線程都在處在 Wait on condition,從線程 stack看, 正等待網絡讀寫,這可能是一個網絡瓶頸的征兆。
因為網絡阻塞導致線程無法執(zhí)行。
一種情況是網絡非常忙,幾乎消耗了所有的帶寬,仍然有大量數據等待網絡讀寫;
另一種情況也可能是網絡空閑,但由于路由等問題,導致包無法正常的到達。
所以要結合系統(tǒng)的一些性能觀察工具來綜合分析,
比如 netstat統(tǒng)計單位時間的發(fā)送包的數目,如果很明顯超過了所在網絡帶寬的限制;
另外一種出現 Wait on condition的常見情況是該線程在 sleep,等待 sleep的時間到了時候,將被喚醒。
###2.in Object.wait() / on object monitor:
說明該線程正在執(zhí)行obj.wait()方法,放棄了 Monitor,進入 “Wait Set”隊列. 那么線程為什么會進入 “Wait Set” 呢?
當線程獲得了 Monitor,進入了臨界區(qū)之后,如果發(fā)現線程繼續(xù)運行的條件沒有滿足,
它則調用對象(一般就是被 synchronized 的對象)的 wait() 方法,放棄了 Monitor,進入 “Wait Set”隊列。
只有當別的線程在該對象上調用了 notify() 或者 notifyAll() , “ Wait Set”隊列中線程才得到機會去競爭,
但是只有一個線程獲得對象的 Monitor,恢復到運行態(tài)。
在 “Wait Set”中的線程, DUMP中表現為: in Object.wait(),類似于:
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000002d34800 nid=0x2b50 in Object.wait() [0x0000000018adf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
仔細觀察上面的 DUMP信息,你會發(fā)現它有以下兩行:
- locked <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
- waiting on <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
這里需要解釋一下,為什么先 lock了這個對象,然后又 waiting on同一個對象呢?讓我們看看這個線程對應的代碼:
synchronized(obj) {
.........
obj.wait();
.........
}
線程的執(zhí)行中,先用 synchronized 獲得了這個對象的 Monitor(對應于 locked <0x00000000d5f06b68> )。
當執(zhí)行到 obj.wait(), 線程即放棄了 Monitor的所有權,進入 “wait set”隊列(對應于 waiting on <0xef63beb8> )。
往往在你的程序中,會出現多個類似的線程,他們都有相似的 DUMP信息。這也可能是正常的。
比如,在程序中,有多個服務線程,設計成從一個隊列里面讀取請求數據。
這個隊列就是 lock 以及 waiting on的對象。
當隊列為空的時候,這些線程都會在這個隊列上等待,直到隊列有了數據,這些線程被 Notify,
當然只有一個線程獲得了 lock,繼續(xù)執(zhí)行,而其它線程繼續(xù)等待。

Blocked(entry set --> waiting for monitor entry) & Waiting(wait set --> in object.wait or on object monitor).png
2.1.x 阻塞和等待的區(qū)別
#阻塞狀態(tài)的線程:
是在等待一個排它鎖,直到別的線程釋放該排它鎖,該線程獲取到該鎖才能退出阻塞狀態(tài);
堆棧信息顯示為waiting for monitor entry, 在Entry Set中.
即代碼尚未走進synchronized塊.
synchronized(obj) {
...
}
#等待狀態(tài)的線程:
則是等待一段時間,由系統(tǒng)喚醒或者別的線程喚醒,該線程便退出等待狀態(tài)。
堆棧信息顯示為 in object.wait() / on object monitor, 在Wait Set 中.
即代碼走到了下文的 obj.wait();
synchronized(obj) {
...
obj.wait();
...
}
2.2 JDK 5.0 的 Lock
上面我們提到如果 synchronized和 monitor 機制運用不當,可能會造成多線程程序的性能問題。
在 JDK 5.0中,引入了 Lock機制,從而使開發(fā)者能更靈活的開發(fā)高性能的并發(fā)多線程程序,
可以替代以往 JDK中的 synchronized和 Monitor的 機制。
但是,要注意的是,因為 Lock類只是一個普通類, JVM無從得知 Lock對象的占用情況,
所以在線程 DUMP中,也不會包含關于 Lock的信息, 關于死鎖等問題,就不如用 synchronized的編程方式容易識別。
http://www.itdecent.cn/p/8a4a519e2f13 (可見此文 4.反例)
2.3 熱鎖
熱鎖,也往往是導致系統(tǒng)性能瓶頸的主要因素。其表現特征為,由于多個線程對臨界區(qū),或者鎖的競爭,可能出現:
>> 頻繁的線程的上下文切換:
從操作系統(tǒng)對線程的調度來看,當線程在等待資源而阻塞的時候,操作系統(tǒng)會將之切換出來,
放到等待的隊列,當線程獲得資源之后,調度算法會將這個線程切換進去,放到執(zhí)行隊列中。
>> 大量的系統(tǒng)調用:
因為線程的上下文切換,以及熱鎖的競爭,或者臨界區(qū)的頻繁的進出,都可能導致大量的系統(tǒng)調用。
>> 大部分 CPU開銷用在 “系統(tǒng)態(tài) ”:
線程上下文切換,和系統(tǒng)調用,都會導致 CPU在 “系統(tǒng)態(tài) ”運行,換而言之,
雖然系統(tǒng)很忙碌,但是 CPU用在 “用戶態(tài) ”的比例較小,應用程序得不到充分的 CPU資源。
>> 隨著 CPU數目的增多,系統(tǒng)的性能反而下降。因為 CPU數目多,同 時運行的線程就越多,
可能就會造成更頻繁的線程上下文切換和系統(tǒng)態(tài)的 CPU開銷,從而導致更糟糕的性能。
從整體的性能指標看,由于線程熱鎖的存在,程序的響應時間會變長,吞吐量會降低。
2.4 哪些線程狀態(tài)占用CPU?
處于TIMED_WAITING、WATING、BLOCKED狀態(tài)的線程是不消耗CPU的,
而處于 RUNNABLE 狀態(tài)的線程要結合當前線程代碼的性質判斷是否消耗CPU:
>> 純java運算代碼,并且未被掛起,是消耗CPU的;
>> 網絡IO操作,在等待數據時是不消耗CPU的;
參考資料
https://docs.oracle.com/cd/E15289_01/JRJDK/using_threaddumps.htm (Oracle官網)
https://www.iteye.com/blog/jameswxx-1041173
https://www.cnblogs.com/yuandengta/p/12900608.html (深入分析Object.notify/wait機制)
https://www.cnblogs.com/perfma/p/12515665.html