注: 該文章的原文是由 Tae Jin Gu 編寫,原文地址爲 How to Analyze Java Thread Dumpsjava
當有障礙,或者是一個基於 JAVA 的 WEB 應用運行的比預期慢的時候,咱們須要使用 thread dumps
。若是對於你來講, thread dumps
是很是複雜的,這篇文章或許能對你有所幫助。在這裏我將解釋在 JAVA 中什麼是 threads
,他們的類型,怎麼被建立的,怎樣管理它們,你怎樣從正在運行的應用中 dump threads
,最後你能夠怎樣分析它以及肯定瓶頸或者是阻塞線程。本文來自於 JAVA 應用程序長期調試經驗的結果。linux
一個 web 服務器使用幾十到幾百個線程來處理大量併發用戶,若是一個或多個線程使用相同的資源,線程之間的競爭就不可避免了,而且有時候可能會發生死鎖。web
Thread contention 是一個線程等待鎖的一個狀態,這個鎖被另一個線程持有,等待被釋放,不一樣的線程頻繁訪問 WEB 應用的共享資源。例如,記錄一條日誌,線程嘗試記錄日誌以前必須先獲取鎖來訪問共享資源。apache
死鎖是線程競爭的一個特殊狀態,一個或是多個線程在等待其餘線程完成它們的任務爲了完成它們本身的任務。編程
線程競爭會引發各類不一樣的問題,爲了分析這些這些問題,你須要使用 dump threads,dump threads
能給你提供每一個線程的精確狀態信息。服務器
一個線程能夠與其餘線程在同一時間內被處理。爲了確保一致性,當多個線程試圖使用共享資源的時候,經過使用 hread synchronization
在同一時間內,應該只有一個線程能訪問共享資源多線程
JAVA 中的線程同步可使用監視器,每一個 JAVA 對象都有一個單獨的監視器,這個監視器僅僅只能被一個線程擁有,對於擁有一個由不一樣的線程所擁有的監視器的線程,確實須要在隊列中等待,以便其餘線程釋放它的監視器。併發
爲了分析一個 thread dump
文件,你須要知道線程狀態。線程狀況在 java.lang.Thread.State
中闡明瞭。oracle
圖1:線程狀態app
WAITING
不一樣是經過方法參數指定了最大等待時間,WAITING
能夠經過時間或者是外部的變化解除)JAVA 的線程類型分爲如下兩種:
Daemon threads 將中止工做當沒有其餘任何非 Daemon threads
時。即便你不建立任何線程,JAVA 應用也將默認建立幾個線程。他們大部分是 daemon threads
。主要用於任務處理好比內存回收或者是 JMX
。
一個運行 static void main(String[] args)
方法的線程被做爲非 daemon threads
線程建立,而且當該線程中止工做的時候,全部任何其餘 daemon threads
也將中止工做。(這個運行在 main 方法中的線程被稱爲 VM thread in HotSpot VM)
咱們將介紹三種最經常使用的方法,記住,有很是多的其餘方法能夠獲取thread dump
,一個 thread dump
僅僅只能在測量的時候顯示線程狀態。所以爲了看得線程狀態的變化,建議每隔5秒提取5到10次的記錄。
在 JDK1.6 或者是更高的版本中,經過使用 jstack, 在 MS Windows 平臺上可能能夠獲取到 Thread Dump
。
經過使用 jps
檢查當前正在運行的JAVA進程的 PID。
[user@linux ~]$ jps -v 25780 RemoteTestRunner -Dfile.encoding=UTF-8 25590 sub.rmi.registry.RegistryImpl 2999 -Dapplication.home=/home1/user/java/jdk.1.6.0_24 -Xms8m 26300 sun.tools.jps.Jps -mlvV -Dapplication.home=/home1/user/java/jdk.1.6.0_24 -Xms8m
使用明確的 PID 做爲 jstack
的參數來獲取 thread dumps
。
[user@linux ~]$ jstack -f 5824
經過使用一個程序 jVisualVM
來生成 Thread Dump
。
如上圖在左側的任務表示當前正在運行的進程列表,點擊你想要信息的那個線程,而後選擇 thread tab
頁來檢查實時的線程信息。點擊右邊的 Thread Dump
按鈕來獲取 thread dump
文件。
經過使用 ps -ef
命令來獲取當前正在運行的 JAVA 應用程序的進程 ID。
[user@linux ~]$ ps - ef | grep java user 2477 1 0 Dec23 ? 00:10:45 ... user 25780 25361 0 15:02 pts/3 00:00:02 ./jstatd -J -Djava.security.policy=jstatd.all.policy -p 2999 user 26335 25361 0 15:49 pts/3 00:00:00 grep java
使用精確的 pid 做爲 kill –SIGQUIT(3)
的參數來獲取 thread dump
。
"pool-1-thread-13" prio=6 tid=0x000000000729a000 nid=0x2fb4 runnable [0x0000000007f0f000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:129) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158) - locked <0x0000000780b7e688> (a java.io.InputStreamReader) at java.io.InputStreamReader.read(InputStreamReader.java:167) at java.io.BufferedReader.fill(BufferedReader.java:136) at java.io.BufferedReader.readLine(BufferedReader.java:299) - locked <0x0000000780b7e688> (a java.io.InputStreamReader) at java.io.BufferedReader.readLine(BufferedReader.java:362)
Java.lang.Thread
類生成一個線程的時候,該線程將被命名爲 Thread-(Number)
。可是當使用 java.util.concurrent.ThreadFactory
類的時候,它將被命名爲 pool-(number)-thread-(number)
。這個應用程序的總體性能降低是由於一個線程佔用了鎖阻止了其餘線程得到鎖,在下面的示例中,BLOCKED_TEST pool-1-thread-1
線程佔用了 <0x0000000780a000b0>
鎖,然而 BLOCKED_TEST pool-1-thread-2
和 BLOCKED_TEST pool-1-thread-3 threads
正在等待獲取鎖。
"BLOCKED_TEST pool-1-thread-1" prio=6 tid=0x0000000006904800 nid=0x28f4 runnable [0x000000000785f000] java.lang.Thread.State: RUNNABLE at java.io.FileOutputStream.writeBytes(Native Method) at java.io.FileOutputStream.write(FileOutputStream.java:282) at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:65) at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:123) - locked <0x0000000780a31778> (a java.io.BufferedOutputStream) at java.io.PrintStream.write(PrintStream.java:432) - locked <0x0000000780a04118> (a java.io.PrintStream) at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:202) at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:272) at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:85) - locked <0x0000000780a040c0> (a java.io.OutputStreamWriter) at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:168) at java.io.PrintStream.newLine(PrintStream.java:496) - locked <0x0000000780a04118> (a java.io.PrintStream) at java.io.PrintStream.println(PrintStream.java:687) - locked <0x0000000780a04118> (a java.io.PrintStream) at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:44) - locked <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState) at com.nbp.theplatform.threaddump.ThreadBlockedState$1.run(ThreadBlockedState.java:7) at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) at java.lang.Thread.run(Thread.java:662) Locked ownable synchronizers: - <0x0000000780a31758> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) "BLOCKED_TEST pool-1-thread-2" prio=6 tid=0x0000000007673800 nid=0x260c waiting for monitor entry [0x0000000008abf000] java.lang.Thread.State: BLOCKED (on object monitor) at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:43) - waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState) at com.nbp.theplatform.threaddump.ThreadBlockedState\$2.run(ThreadBlockedState.java:26) at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) at java.util.concurrent.ThreadPoolExecutor\$Worker.run(ThreadPoolExecutor.java:908) at java.lang.Thread.run(Thread.java:662) Locked ownable synchronizers: - <0x0000000780b0c6a0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) "BLOCKED_TEST pool-1-thread-3" prio=6 tid=0x00000000074f5800 nid=0x1994 waiting for monitor entry [0x0000000008bbf000] java.lang.Thread.State: BLOCKED (on object monitor) at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:42) - waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState) at com.nbp.theplatform.threaddump.ThreadBlockedState\$3.run(ThreadBlockedState.java:34) at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) at java.lang.Thread.run(Thread.java:662) Locked ownable synchronizers: - <0x0000000780b0e1b8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
這是當線程 A 須要獲取線程 B 的鎖來繼續它的任務,然而線程 B 也須要獲取線程 A 的鎖來繼續它的任務的時候發生的。在 thread dump
中,你能看到 DEADLOCK_TEST-1
線程持有 0x00000007d58f5e48
鎖,而且嘗試獲取 0x00000007d58f5e60
鎖。你也能看到 DEADLOCK_TEST-2
線程持有 0x00000007d58f5e60
,而且嘗試獲取 0x00000007d58f5e78
,同時 DEADLOCK_TEST-3
線程持有 0x00000007d58f5e78
,而且在嘗試獲取 0x00000007d58f5e48
鎖,如你所見,每一個線程都在等待獲取另一個線程的鎖,這狀態將不會被改變直到一個線程丟棄了它的鎖。
"DEADLOCK_TEST-1" daemon prio=6 tid=0x000000000690f800 nid=0x1820 waiting for monitor entry [0x000000000805f000] java.lang.Thread.State: BLOCKED (on object monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197) - waiting to lock <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182) - locked <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135) Locked ownable synchronizers: - None "DEADLOCK_TEST-2" daemon prio=6 tid=0x0000000006858800 nid=0x17b8 waiting for monitor entry [0x000000000815f000] java.lang.Thread.State: BLOCKED (on object monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197) - waiting to lock <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182) - locked <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135) Locked ownable synchronizers: - None "DEADLOCK_TEST-3" daemon prio=6 tid=0x0000000006859000 nid=0x25dc waiting for monitor entry [0x000000000825f000] java.lang.Thread.State: BLOCKED (on object monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197) - waiting to lock <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182) - locked <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor) at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135) Locked ownable synchronizers: - None
該線程是正常的,由於它的狀態爲 RUNNABLE,儘管如此,當你按照時間順序排列 Thread Dump
,你會發現 socketReadThread
線程正在無限等待讀取 socket。
"socketReadThread" prio=6 tid=0x0000000006a0d800 nid=0x1b40 runnable [0x00000000089ef000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:129) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158) - locked <0x00000007d78a2230> (a java.io.InputStreamReader) at sun.nio.cs.StreamDecoder.read0(StreamDecoder.java:107) - locked <0x00000007d78a2230> (a java.io.InputStreamReader) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:93) at java.io.InputStreamReader.read(InputStreamReader.java:151) at com.nbp.theplatform.threaddump.ThreadSocketReadState$1.run(ThreadSocketReadState.java:27) at java.lang.Thread.run(Thread.java:662)
線程保持在 Waiting
狀態,在 Thread Dump
中,IoWaitThread
線程保持等待狀態來從 LinkedBlockingQueue
接收消息。若是 LinkedBlockingQueue
一直沒有消息,該線程的狀態將不會改變。
"IoWaitThread" prio=6 tid=0x0000000007334800 nid=0x2b3c waiting on condition [0x000000000893f000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007d5c45850> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1987) at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:440) at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:629) at com.nbp.theplatform.threaddump.ThreadIoWaitState$IoWaitHandler2.run(ThreadIoWaitState.java:89) at java.lang.Thread.run(Thread.java:662)
沒必要要的線程會堆積起來,當線程的資源不能被正常的組織的話,若是這個發送了,建議監控線程組織過程或檢查線程終止的條件。
[user@linux ~]$ ps -mo pid.lwp.stime.time.cpu -C java PID LWP STIME TIME %CPU 10029 - Dec07 00:02:02 99.5 - 10039 Dec07 00:00:00 0.1 - 10040 Dec07 00:00:00 95.5
從這個應用中,發現使用 CPU 最高的線程。
獲取使用 CPU 最多的輕量級進程(LWP),把它的惟一標示碼 (10039) 轉換成十六進制 (0x2737)。
Thread Dump
,檢查進程的動做。經過 PID 10029 來提取應用程序的 Thread Dump
,而後經過一個 nid 0x2737 來找到這個線程。
"NioProcessor-2" prio=10 tid=0x0a8d2800 nid=0x2737 runnable [0x49aa5000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:210) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:65) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:69) - locked <0x74c52678> (a sun.nio.ch.Util$1) - locked <0x74c52668> (a java.util.Collections$UnmodifiableSet) - locked <0x74c501b0> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:80) at external.org.apache.mina.transport.socket.nio.NioProcessor.select(NioProcessor.java:65) at external.org.apache.mina.common.AbstractPollingIoProcessor$Worker.run(AbstractPollingIoProcessor.java:708) at external.org.apache.mina.util.NamePreservingRunnable.run(NamePreservingRunnable.java:51) at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) at java.lang.Thread.run(Thread.java:662)
每一個小時的幾個時間提取 Thread Dump
,而後檢查線程的狀態來肯定問題。
屢次得到 thread dumps
後,找出 BLOCKED
狀態的線程列表。
" DB-Processor-13" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f000] java.lang.Thread.State: BLOCKED (on object monitor) at beans.ConnectionPool.getConnection(ConnectionPool.java:102) - waiting to lock <0xe0375410> (a beans.ConnectionPool) at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111) at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43) "DB-Processor-14" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f020] java.lang.Thread.State: BLOCKED (on object monitor) at beans.ConnectionPool.getConnection(ConnectionPool.java:102) - waiting to lock <0xe0375410> (a beans.ConnectionPool) at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111) at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43) " DB-Processor-3" daemon prio=5 tid=0x00928248 nid=0x8b waiting for monitor entry [0x000000000825d080] java.lang.Thread.State: RUNNABLE at oracle.jdbc.driver.OracleConnection.isClosed(OracleConnection.java:570) - waiting to lock <0xe03ba2e0> (a oracle.jdbc.driver.OracleConnection) at beans.ConnectionPool.getConnection(ConnectionPool.java:112) - locked <0xe0386580> (a java.util.Vector) - locked <0xe0375410> (a beans.ConnectionPool) at beans.cus.Cue_1700c.GetNationList(Cue_1700c.java:66) at org.apache.jsp.cue_1700c_jsp._jspService(cue_1700c_jsp.java:120)
在屢次獲取 thread dumps
後,取得 BLOCKED
狀態的線程列表。
若是線程是 BLOCKED
的,提取線程嘗試獲取的相關聯的鎖。
經過 thread dumps
,你能肯定線程狀態中止在 BLOCKED
,由於鎖 <0xe0375410>
不能被獲取到,這個問題能夠經過分析當前夯住的線程的 stack trace
來解決。
使用 DBMS
的時候,爲何以上的範例常常出現再應用程序中,這有兩個緣由。第一個緣由是配置不當。儘管事實是該線程仍然在工做,它們不能展現它們最好的性能,由於 DBCP
的配置文件沒有配置正確。若是你屢次提取 thread dumps
而且對比它們,你將常常看到被阻塞的線程以前處於不一樣的狀態。
第二個緣由是不正常的鏈接。當與 DBMS
的鏈接保持在不正常的狀態,線程將等待直到超時。在這個例子中,經過屢次提取 thread dumps
並對比它們,你會發現與 DBMS 相關的線程仍然在阻塞狀態。經過適當改變一些值,好比超時時間,你能夠縮短問題發生的時間。
當使用 java.lang.Thread
對象建立線程的時候,線程被命名爲 Thread-(Number) 。當使用 java.util.concurrent.DefaultThreadFactory
對象建立線程的時候,線程被命名爲 named pool-(Number)-thread-(Number)。當爲應用程序分析成百上千的線程的時候,若是線程依然用它們默認的名字,分析它們將變得很是困難,由於這是很是難以辨別這些線程來分析的。
所以,你被建議開發一個命名線程的規則當一個新線程被建立的時候。
當你使用 java.lang.Thread
建立線程,你能夠經過建立參數給該線程定義個約定俗成的名字。
public Thread(Runnable target, String name); public Thread(ThreadGroup group, String name); public Thread(ThreadGroup group, Runnable target, String name); public Thread(ThreadGroup group, Runnable target, String name, long stackSize);
當你使用 java.util.concurrent.ThreadFactory
建立線程的時候,你能夠經過生成你本身的線程工廠來命名它,若是你不須要特別的功能性,你可使用 MyThreadFactory
做爲如下描述:
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; public class MyThreadFactory implements ThreadFactory { private static final ConcurrentHashMap<String, AtomicInteger> POOL_NUMBER = new ConcurrentHashMap<String, AtomicInteger>(); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; public MyThreadFactory(String threadPoolName) { if (threadPoolName == null) { throw new NullPointerException("threadPoolName"); } POOL_NUMBER.putIfAbsent(threadPoolName, new AtomicInteger()); SecurityManager securityManager = System.getSecurityManager(); group = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); AtomicInteger poolCount = POOL_NUMBER.get(threadPoolName); if (poolCount == null) { namePrefix = threadPoolName + " pool-00-thread-"; } else { namePrefix = threadPoolName + " pool-" + poolCount.getAndIncrement() + "-thread-"; } } public Thread newThread(Runnable runnable) { Thread thread = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0); if (thread.isDaemon()) { thread.setDaemon(false); } if (thread.getPriority() != Thread.NORM_PRIORITY) { thread.setPriority(Thread.NORM_PRIORITY); } return thread; } }
你可使用 MBean 來獲取 ThreadInfo
對象。你也能夠獲取更加多經過 thread dumps 不能獲取的信息。經過使用 ThreadInfo
。
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); long[] threadIds = mxBean.getAllThreadIds(); ThreadInfo[] threadInfos = mxBean.getThreadInfo(threadIds); for (ThreadInfo threadInfo : threadInfos) { System.out.println( threadInfo.getThreadName()); System.out.println( threadInfo.getBlockedCount()); System.out.println( threadInfo.getBlockedTime()); System.out.println( threadInfo.getWaitedCount()); System.out.println( threadInfo.getWaitedTime()); }
你可使用方法 ThreadInfo
來提取阻塞線程或者是等待線程花費的時間。並利用這一點,你也能夠獲得那些處於非活動狀態的時間異常長的線程列表。
在本文中,我關注的是爲開發人員提供了大量的多線程編程經驗,本素材多是常識。對於經驗較少的開發人員來講,我以爲我直接跳過 thread dumps
,不提供足夠的關於 thread activities
的背景知識。這是因爲個人知識缺少,因此我不能很清晰的簡潔明瞭的解釋 thread activities
。我衷心的但願本文能給不少開發人員提供幫助。