Java 的線(xiàn)程線(xiàn)程是指能獨立于程序的其它部分運行的執行單元。 JAVA語(yǔ)言能夠很好的實(shí)現多線(xiàn)程的程序。我們在調試程序,或者在開(kāi)發(fā)后期需要做性能調優(yōu)的時(shí)候,往往也需要了解當前程序正在運行的線(xiàn)程的狀態(tài),正在執行的操作,從而分析系統可能存在的問(wèn)題。 在閱讀本文之間,應對 Java線(xiàn)程的編程原理,同步機制有一定了解 . 產(chǎn)生 JAVA線(xiàn)程 dumpJAVA 的線(xiàn)程 DUMP,就象當前 JAVA進(jìn)程的一個(gè)快照,打印出所有線(xiàn)程的狀態(tài)和調用堆棧,以及 Monitor的狀態(tài)。在不同的操作系統下,產(chǎn)生線(xiàn)程 DUMP的方式是不同的。
用 “kill -3 <pid>” ,或者 “kill – QUIT <pid>”。 Pid是用所關(guān)注的 JAVA進(jìn)程號,您可以用 “ps -ef | grep java” 找到,或者使用 JDK 5.0中的 “jps -v” 命令獲得。
這里要注意的是: 1. 不同的 JAVA虛機的線(xiàn)程 DUMP的創(chuàng )建方法和文件格式是不一樣的,不同的 JVM版本, dump信息也有差別。本文中,只以 SUN的 hotspot JVM 5.0_06 為例。 2. 在實(shí)際運行中,往往一次 dump的信息,還不足以確認問(wèn)題。建議產(chǎn)生三次 dump信息,如果每次 dump都指向同一個(gè)問(wèn)題,我們才確定問(wèn)題的典型性。 線(xiàn)程分析 :1. JVM 線(xiàn)程 在線(xiàn)程中,有一些 JVM內部的后臺線(xiàn)程,來(lái)執行譬如垃圾回收,或者低內存的檢測等等任務(wù),這些線(xiàn)程往往在 JVM初始化的時(shí)候就存在,如下所示: "Low Memory Detector" daemon prio=10 tid=0x081465f8 nid=0x7 runnable [0x00000000..0x00000000] "CompilerThread0" daemon prio=10 tid=0x08143c58 nid=0x6 waiting on condition [0x00000000..0xfb5fd798] "Signal Dispatcher" daemon prio=10 tid=0x08142f08 nid=0x5 waiting on condition [0x00000000..0x00000000] "Finalizer" daemon prio=10 tid=0x08137ca0 nid=0x4 in Object.wait() [0xfbeed000..0xfbeeddb8] at java.lang.Object.wait(Native Method) - waiting on <0xef600848> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116) - locked <0xef600848> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159) "Reference Handler" daemon prio=10 tid=0x081370f0 nid=0x3 in Object.wait() [0xfbf4a000..0xfbf4aa38] at java.lang.Object.wait(Native Method) - waiting on <0xef600758> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:474) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116) - locked <0xef600758> (a java.lang.ref.Reference$Lock) "VM Thread" prio=10 tid=0x08134878 nid=0x2 runnable "VM Periodic Task Thread" prio=10 tid=0x08147768 nid=0x8 waiting on condition 我們更多的是要觀(guān)察用戶(hù)級別的線(xiàn)程,如下所示: "Thread-1" prio=10 tid=0x08223860 nid=0xa waiting on condition [0xef47a000..0xef47ac38] at java.lang.Thread.sleep(Native Method) at testthread.MySleepingThread.method2(MySleepingThread.java:53) - locked <0xef63d600> (a testthread.MySleepingThread) at testthread.MySleepingThread.run(MySleepingThread.java:35) at java.lang.Thread.run(Thread.java:595) 我們能看到:
這些信息對我們隨后的分析都有用處。 2. 線(xiàn)程的狀態(tài)分析 正如我們剛看到的那樣,線(xiàn)程的狀態(tài)是一個(gè)重要的指標,它會(huì )顯示在線(xiàn)程 Stacktrace的頭一行結尾的地方。那么線(xiàn)程常見(jiàn)的有哪些狀態(tài)呢?線(xiàn)程在什么樣的情況下會(huì )進(jìn)入這種狀態(tài)呢?我們能從中發(fā)現什么線(xiàn)索?< /span> 1.1 Runnable 該狀態(tài)表示線(xiàn)程具備所有運行條件,在運行隊列中準備操作系統的調度,或者正在運行。 1.2 Wait on condition 該狀態(tài)出現在線(xiàn)程等待某個(gè)條件的發(fā)生。具體是什么原因,可以結合 stacktrace來(lái)分析。最常見(jiàn)的情況是線(xiàn)程在等待網(wǎng)絡(luò )的讀寫(xiě),比如當網(wǎng)絡(luò )數據沒(méi)有準備好讀時(shí),線(xiàn)程處于這種等待狀態(tài),而一旦有數據準備好讀之后,線(xiàn)程會(huì )重新激活,讀取并處理數據。在 Java引入 NewIO之前,對于每個(gè)網(wǎng)絡(luò )連接,都有一個(gè)對應的線(xiàn)程來(lái)處理網(wǎng)絡(luò )的讀寫(xiě)操作,即使沒(méi)有可讀寫(xiě)的數據,線(xiàn)程仍然阻塞在讀寫(xiě)操作上,這樣有可能造成資源浪費,而且給操作系統的線(xiàn)程調度也帶來(lái)壓力。在 NewIO里采用了新的機制,編寫(xiě)的服務(wù)器程序的性能和可擴展性都得到提高。 如果發(fā)現有大量的線(xiàn)程都在處在 Wait on condition,從線(xiàn)程 stack看, 正等待網(wǎng)絡(luò )讀寫(xiě),這可能是一個(gè)網(wǎng)絡(luò )瓶頸的征兆。因為網(wǎng)絡(luò )阻塞導致線(xiàn)程無(wú)法執行。一種情況是網(wǎng)絡(luò )非常忙,幾 乎消耗了所有的帶寬,仍然有大量數據等待網(wǎng)絡(luò )讀 寫(xiě);另一種情況也可能是網(wǎng)絡(luò )空閑,但由于路由等問(wèn)題,導致包無(wú)法正常的到達。所以要結合系統的一些性能觀(guān)察工具來(lái)綜合分析,比如 netstat統計單位時(shí)間的發(fā)送包的數目,如果很明顯超過(guò)了所在網(wǎng)絡(luò )帶寬的限制 ; 觀(guān)察 cpu的利用率,如果系統態(tài)的 CPU時(shí)間,相對于用戶(hù)態(tài)的 CPU時(shí)間比例較高;如果程序運行在 Solaris 10平臺上,可以用 dtrace工具看系統調用的情況,如果觀(guān)察到 read/write的系統調用的次數或者運行時(shí)間遙遙領(lǐng)先;這些都指向由于網(wǎng)絡(luò )帶寬所限導致的網(wǎng)絡(luò )瓶頸。 另外一種出現 Wait on condition的常見(jiàn)情況是該線(xiàn)程在 sleep,等待 sleep的時(shí)間到了時(shí)候,將被喚醒。 1.3 Waiting for monitor entry 和 in Object.wait() 在多線(xiàn)程的 JAVA程序中,實(shí)現線(xiàn)程之間的同步,就要說(shuō)說(shuō) Monitor。 Monitor是 Java中用以實(shí)現線(xiàn)程之間的互斥與協(xié)作的主要手段,它可以看成是對象或者 Class的鎖。每一個(gè)對象都有,也僅有一個(gè) monitor。下 面這個(gè)圖,描述了線(xiàn)程和 Monitor之間關(guān)系,以 及線(xiàn)程的狀態(tài)轉換圖: ![]() 從圖中可以看出,每個(gè) Monitor在某個(gè)時(shí)刻,只能被一個(gè)線(xiàn)程擁有,該線(xiàn)程就是 “Active Thread”,而其它線(xiàn)程都是 “Waiting Thread”,分別在兩個(gè)隊列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的線(xiàn)程狀態(tài)是 “Waiting for monitor entry”,而在 “Wait Set”中等待的線(xiàn)程狀態(tài)是 “in Object.wait()”。 先看 “Entry Set”里面的線(xiàn)程。我們稱(chēng)被 synchronized保護起來(lái)的代碼段為臨界區。當一個(gè)線(xiàn)程申請進(jìn)入臨界區時(shí),它就進(jìn)入了 “Entry Set”隊列。對應的 code就像: synchronized(obj) { ......... } 這時(shí)有兩種可能性: · 該 monitor不被其它線(xiàn)程擁有, Entry Set里面也沒(méi)有其它等待線(xiàn)程。本線(xiàn)程即成為相應類(lèi)或者對象的 Monitor的 Owner,執行臨界區的代碼 · 該 monitor被其它線(xiàn)程擁有,本線(xiàn)程在 Entry Set隊列中等待。 在第一種情況下,線(xiàn)程將處于 “Runnable”的狀態(tài),而第二種情況下,線(xiàn)程 DUMP會(huì )顯示處于 “waiting for monitor entry”。如下所示: "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) 臨界區的設置,是為了保證其內部的代碼執行的原子性和完整性。但是因為臨界區在任何時(shí)間只允許線(xiàn)程串行通過(guò),這 和我們多線(xiàn)程的程序的初衷是相反的。 如果在多線(xiàn)程的程序中,大量使用 synchronized,或者不適當的使用了它,會(huì )造成大量線(xiàn)程在臨界區的入口等待,造成系統的性能大幅下降。如果在線(xiàn)程 DUMP中發(fā)現了這個(gè)情況,應該審查源碼,改進(jìn)程序。 現在我們再來(lái)看現在線(xiàn)程為什么會(huì )進(jìn)入 “Wait Set”。當線(xiàn)程獲得了 Monitor,進(jìn)入了臨界區之后,如果發(fā)現線(xiàn)程繼續運行的條件沒(méi)有滿(mǎn)足,它則調用對象(一般就是被 synchronized 的對象)的 wait() 方法,放棄了 Monitor,進(jìn)入 “Wait Set”隊列。只有當別的線(xiàn)程在該對象上調用了 notify() 或者 notifyAll() , “ Wait Set”隊列中線(xiàn)程才得到機會(huì )去競爭,但是只有一個(gè)線(xiàn)程獲得對象的 Monitor,恢復到運行態(tài)。在 “Wait Set”中的線(xiàn)程, DUMP中表現為: in Object.wait(),類(lèi)似于: "Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38] at java.lang.Object.wait(Native Method) - waiting on <0xef63beb8> (a java.util.ArrayList) at java.lang.Object.wait(Object.java:474) at testthread.MyWaitThread.run(MyWaitThread.java:40) - locked <0xef63beb8> (a java.util.ArrayList) at java.lang.Thread.run(Thread.java:595) 仔細觀(guān)察上面的 DUMP信息,你會(huì )發(fā)現它有以下兩行: - locked <0xef63beb8> (a java.util.ArrayList) - waiting on <0xef63beb8> (a java.util.ArrayList) 這里需要解釋一下,為什么先 lock了這個(gè)對象,然后又 waiting on同一個(gè)對象呢?讓我們看看這個(gè)線(xiàn)程對應的代碼: synchronized(obj) { ......... obj.wait(); ......... } 線(xiàn)程的執行中,先用 synchronized 獲得了這個(gè)對象的 Monitor(對應于 locked <0xef63beb8> )。當執行到 obj.wait(), 線(xiàn)程即放棄了 Monitor的所有權,進(jìn)入 “wait set”隊列(對應于 waiting on <0xef63beb8> )。 往往在你的程序中,會(huì )出現多個(gè)類(lèi)似的線(xiàn)程,他們都有相似的 DUMP信息。這也可能是正常的。比如,在程序中,有多個(gè)服務(wù)線(xiàn)程,設計成從一個(gè)隊列里面讀取請求數據。這個(gè)隊列就是 lock以及 waiting on的對象。當隊列為空的時(shí)候,這些線(xiàn)程都會(huì )在這個(gè)隊列上等待,直到隊列有了數據,這些線(xiàn)程被 Notify,當然只有一個(gè)線(xiàn)程獲得了 lock,繼續執行,而其它線(xiàn)程繼續等待。 3. JDK 5.0 的 lock 上面我們提到如果 synchronized和 monitor機制運用不當,可能會(huì )造成多線(xiàn)程程序的性能問(wèn)題。在 JDK 5.0中,引入了 Lock機制,從而使開(kāi)發(fā)者能更靈活的開(kāi)發(fā)高性能的并發(fā)多線(xiàn)程程序,可以替代以往 JDK中的 synchronized和 Monitor的 機制。但是,要注意的是,因為 Lock類(lèi)只是一個(gè)普通類(lèi), JVM無(wú)從得知 Lock對象的占用情況,所以在線(xiàn)程 DUMP中,也不會(huì )包含關(guān)于 Lock的信息, 關(guān)于死鎖等問(wèn)題,就不如用 synchronized的編程方式容易識別。 案例分析1. 死鎖 在多線(xiàn)程程序的編寫(xiě)中,如果不適當的運用同步機制,則有可能造成程序的死鎖,經(jīng)常表現為程序的停頓,或者不再響應用戶(hù)的請求。 比如在下面這個(gè)示例中,是個(gè)較為典型的死鎖情況: "Thread-1" prio=5 tid=0x00acc490 nid=0xe50 waiting for monitor entry [0x02d3f000 ..0x02d3fd68] at deadlockthreads.TestThread.run(TestThread.java:31) - waiting to lock <0x22c19f18> (a java.lang.Object) - locked <0x22c19f20> (a java.lang.Object) "Thread-0" prio=5 tid=0x00accdb0 nid=0xdec waiting for monitor entry [0x02cff000 ..0x02cff9e8] at deadlockthreads.TestThread.run(TestThread.java:31) - waiting to lock <0x22c19f20> (a java.lang.Object) - locked <0x22c19f18> (a java.lang.Object) 在 JAVA 5中加強了對死鎖的檢測。線(xiàn)程 Dump中可以直接報告出 Java級別的死鎖,如下所示: Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x0003f334 (object 0x22c19f18, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x0003f314 (object 0x22c19f20, a java.lang.Object), which is held by "Thread-1" 2. 熱鎖 熱鎖,也往往是導致系統性能瓶頸的主要因素。其表現特征為,由于多個(gè)線(xiàn)程對臨界區,或者鎖的競爭,可能出現:& amp; lt; /span>
上面的描述,都是一個(gè) scalability(可擴展性)很差的系統的表現。從整體的性能指標看,由于線(xiàn)程熱鎖的存在,程序的響應時(shí)間會(huì )變長(cháng),吞吐量會(huì )降低。< /span> 那么,怎么去了解 “熱鎖 ”出現在什么地方呢?一個(gè)重要的方法還是結合操作系統的各種工具觀(guān)察系統資源使用狀況,以及收集 Java線(xiàn)程的 DUMP信息,看線(xiàn)程都阻塞在什么方法上,了解原因,才能找到對應的解決方法。 我們曾經(jīng)遇到過(guò)這樣的例子,程序運行時(shí),出現了以上指出的各種現象,通過(guò)觀(guān)察操作系統的資源使用統計信息,以及線(xiàn)程 DUMP信息,確定了程序中熱鎖的存在,并發(fā)現大多數的線(xiàn)程狀態(tài)都是 Waiting for monitor entry或者 Wait on monitor,且是阻塞在壓縮和解壓縮的方法上。后來(lái)采用第三方的壓縮包 javalib替代 JDK自帶的壓縮包后,系統的性能提高了幾倍。 總結本文就介紹了 Java線(xiàn)程 DUMP的基本知識和分析的基本方法,并且解釋了如何利用線(xiàn)程的 DUMP信息,以及結合操作系統的各種資源使用情況,分析程序的性能問(wèn)題,從而達到改進(jìn)程序,提高性能的目的。 |
聯(lián)系客服