Java性能分析之線程棧詳解(下)
結合jstack結果對線程狀態詳解
上篇文章詳細介紹了線程棧的作用、狀態、任何查看理解,本篇文章結合jstack工具來查看線程狀態,並列出重點關注目標。Jstack是常用的排查工具,它能輸出在某一個時間,Java進程中所有線程的狀態,很多時候這些狀態信息能給我們的排查工作帶來有用的線索。 Jstack的輸出中,Java線程狀態主要是以下幾種:
1、BLOCKED 線程在等待monitor鎖(synchronized關鍵字)
2、TIMED_WAITING 線程在等待喚醒,但設置了時限
3、WAITING 線程在無限等待喚醒
4、RUNNABLE 線程運行中或I/O等待
下面通過詳細的實例來對這幾種狀態進行解釋
BLOCKED
如下圖所示,為使用jstack工具dump線程后,查看到的線程處於blocked狀態。dump線程后,最先看的是線程所處的狀態。這個線程處於Blocked狀態,我們需要重點分析。

首先,我們來逐條分析下jstack工具抓取到的線程信息:
jstack工具抓取到的線程信息,是從下往上分析的,由上圖可見,線程先是開始運行,之后運行業務的一些方法,直到調用 org.apache.log4j.Category.forcedLog之后,開始waiting to lock。
線程的狀態是:BLOCKED (on object monitor)
說明線程處於阻塞狀態,正在等待一個monitor lock。阻塞原因是:因為本線程與其他線程公用了一個鎖,這時,已經有其他在線程正在使用這個鎖進入某個synchronized同步方法塊或者方法。當本線程想要進入這個同步代碼塊時,也需要這個鎖,但鎖已被占用,從而導致本線程處於阻塞狀態。
第一行中包含了線程名和id等信息,如上圖中的"druid-consumer-pool-3",nid(每個線程都有線程pid,將該pid轉成16進制的值,即為jstack結果中的nid,可以通過nid唯一確認一個線程。)
第一行中還有線程目前正在 waiting for monitor entry,還是表明了線程在等待進入monitor。
Monitor是 Java中用以實現線程之間的互斥與協作的主要手段,它可以看成是對象或者 Class的鎖。每一個對象都有,也僅有一個 monitor。每個 Monitor在某個時刻,只能被一個線程擁有,該線程就是 “Active Thread”,而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的線程狀態是 “Waiting for monitor entry”,而在 “Wait Set”中等待的線程狀態是 “in Object.wait()”。目前線程狀態為:waiting for monitor entry,說明它是“Entry Set”里面的線程。我們稱被 synchronized保護起來的代碼段為臨界區。當一個線程申請進入臨界區時,它就進入了 “Entry Set”隊列。
這時有兩種可能性:
1、該 monitor不被其它線程擁有, Entry Set里面也沒有其它等待線程。本線程即成為相應類或者對象的 Monitor的 Owner,執行臨界區的代碼
2、該 monitor被其它線程擁有,本線程在 Entry Set隊列中等待。
在第一種情況下,線程將處於 “Runnable”的狀態
而第二種情況下,線程 DUMP會顯示處於 “waiting for monitor entry”
根據以上分析,我們可以看出,線程想要調用log4j,目的是打印日志,但是由於調用log4j寫日志有鎖機制,於是線程被阻塞了。再排查項目使用的log4j版本,得知此版本存在性能bug,優化手段為升級log4j版本或者調整日志級別、優化日志打印的內容,或者添加緩存。
waiting to lock <地址>
說明線程使用synchronized申請對象鎖未成功,於是開始等待別的線程釋放鎖。線程在監視器的進入區等待。這條一般在調用棧頂出現,線程狀態一般對應為Blocked。
TIMED_WAITING
如下圖所示,為使用jstack工具dump線程后,查看到的線程處於TIMED_WAITING狀態。

線程的狀態是:TIMED_WAITING
這時的線程處於sleep狀態,說明線程在有時限的等待另一個線程的特定操作,一般會有超時時間喚醒。就一般情況來說,出現TIMED_WAITING很正常,等待網絡IO等都會出現這種狀態,但是大量的線程處於TIMED_WAITING時,需要我們重點分析。
第一行中,顯示線程在waiting on condition,這說明線程在等待某個條件的發生,從而自己喚醒,或者是調用了 sleep(n)。
當線程在waiting on condition時,線程狀態可能為:
1、java.lang.Thread.State: WAITING (parking):一直等某個條件發生;
2、java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定時等待某個條件發生,即使這個條件不到來,也將定時喚醒自己。
在我們這個例子里,線程處於 TIMED_WAITING狀態。
parking to wait for <地址>目標
這里即為第一行“waiting on condition" 所等待的條件,等待是java.util.concurrent.CountDownLatch$Sync,這是一種閉鎖的實現,是一種同步工具類,可以延遲線程的進度直到閉鎖到達終止狀態,其內部包含一個計數器,該計數器被初始化為一個整數,表示需要等待事件的數量。由以上分析可以知道,線程是因為向druid寫數據,由於有同步機制,而進入TIMED_WAITING狀態。

和上個例子線程在parking to wait for 不同,在這個例子中,線程也是處於TIMED_WAITING狀態,但是第一行中顯示線程正在 in Object.wait(),第四行顯示線程waiting on <地址> 目標。
線程在in Object.wait(), 說明線程在獲得了監視器之后,又調用了 java.lang.Object.wait() 方法。
上篇線程詳解(一)中說過等待monitor 的線程分為兩種
在 “Entry Set”中等待的線程狀態是 “Waiting for monitor entry”
在 “Wait Set”中等待的線程狀態是 “in Object.wait()”
本例是在“Wait Set”中等待的線程,其狀態是in Object.wait(),這說明線程獲得了 Monitor,但是線程繼續運行的條件沒有滿足,則調用對象(一般就是被 synchronized 的對象)的 wait() 方法,放棄了 Monitor,進入 “Wait Set”隊列。
此時線程狀態大致為以下幾種:
1、java.lang.Thread.State: TIMED_WAITING (on object monitor);
2、java.lang.Thread.State: WAITING (on object monitor);
本例中線程就處於TIMED_WAITING狀態。
WAITING
如下圖所示,為使用jstack工具dump線程后,查看到的線程處於WAITING狀態。

(1)線程的狀態是:WAITING
意思就是線程在等待另外一個線程去解除它的等待狀態。一個典型的例子就是生產者消費者模型,當生產者生產太慢的時候,消費者要等待生產者生產才能去消費,這段時間消費者線程就處於waiting狀態。還可以使用lock.wait()方法使線程進入waiting狀態,無超時的等待,必須等待lock.notify()或lock.notifyAll()或接收到interrupt信號才能退出等待狀態。
(2)parking to wait for <地址> 目標
第一行中,顯示線程在waiting on condition,這說明線程在等待某個條件的發生,從而自己喚醒。
當線程在waiting on condition時,線程狀態可能為
java.lang.Thread.State: WAITING (parking):一直等某個條件發生;
java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定時等待某個條件發生,即使這個條件不到來,也將定時喚醒自己。
在這個例子里,線程處於 WAITING狀態,parking to wait for所等待的是java.util.concurrent.locks.AbstractQueuedSynchronizer,這也是java實現同步機制。
RUNNABLE
如下圖所示,為使用jstack工具dump線程后,查看到的線程處於RUNNABLE 狀態。

在這個例子里,可以清楚看到整個線程運行的過程。在線程運行過程中,有很多次獲取鎖,即為上圖中locked <地址> 目標,即此線程使用synchronized申請對象鎖成功,是監視器的擁有者,可以在臨界區內進行操作。上圖所lock的內容有java IO的輸入輸出流等。
"02'
在一次測試過程中,通過線程打印有了一個意外收獲
如下面信息,“http-bio-18272-exec-258”,表示Tomcat 的啟動模式為 bio模式,將bio模式改為nio模式,在該項目中,其他條件不變,只將bio模式更改為nio模式,tps提升了一倍

tomcat的運行模式有3種.修改他們的運行模式.3種模式的運行是否成功,可以看他的啟動控制台,或者啟動日志.或者登錄他們的默認頁面http://localhost:8080/查看其中的服務器狀態。
1)bio :默認的模式,性能非常低下,沒有經過任何優化處理和支持.
2)nio :利用java的異步io護理技術,no blocking IO技術.
想運行在該模式下,直接修改server.xml里的Connector節點,修改protocol為
<Connector port="80" protocol="org.apache.coyote.http11.Http11NioProtocol"connectionTimeout="20000" URIEncoding="UTF-8" useBodyEncodingForURI="true"enableLookups="false" redirectPort="8443" />
啟動后,就可以生效。
3)apr
安裝起來最困難,但是從操作系統級別來解決異步的IO問題,大幅度的提高性能.
必須要安裝apr和native,直接啟動就支持apr。
想獲取更多測試技能,歡迎加入BestTest5000人交流群:435092293
