线程转储是什么,如何分析它们?
让我们来谈谈线程转储及其分析方法。
我们还将讨论它如何帮助精确定位问题以及一些可以使用的分析工具。
什么是线程?
进程是加载到计算机内存并正在执行的计算机程序。它可以由一个或多个处理器执行。进程在内存中用重要信息来描述,例如变量存储、文件句柄、程序计数器、寄存器、信号等等。
进程可以由许多称为“线程”的轻量级进程组成。这有助于实现并行处理,其中一个进程被分为多个线程。这可以提高性能。进程中的所有线程共享相同的内存空间,并且彼此之间是相互依赖的。
线程转储
当进程执行时,我们可以使用线程转储检测进程中线程的当前执行状态。线程转储包含程序执行过程中某个特定时间点上所有活动线程的快照。它包含有关线程及其当前状态的所有相关信息。
现代应用程序今天涉及多个线程。每个线程都需要一定的资源,执行与进程相关的某些活动。这可以提高应用程序的性能,因为线程可以利用可用的cpu核心。
但这也存在一些折衷,例如,有时多个线程可能无法良好协调,可能会导致死锁情况。所以,如果出现问题,我们可以使用线程转储来检查线程的状态。
java中的线程转储
jvm线程转储是进程中所有线程在特定时间点的状态列表。它包含有关线程的堆栈信息,以堆栈跟踪的形式呈现。由于它以纯文本形式编写,可以将其内容保存以供以后查看。分析线程转储可以帮助以下方面:
- 优化jvm性能
- 优化应用程序性能
- 诊断问题,例如死锁、线程争用等。
生成线程转储
有许多方法可以生成线程转储。以下是一些基于jvm的工具,可以从命令行/终端(cli工具)或java安装文件夹的/bin(gui工具)目录中执行。
让我们来探索一下它们。
#1. jstack
生成线程转储的最简单方法是使用jstack。jstack随jvm一起提供,并可以从命令行使用。在这里,我们需要进程的pid来生成线程转储。我们可以使用以下命令来获取pid。
jps -l
jps
会列出所有java进程的id。
在windows上
c:program filesjavajdk1.8.0_171bin>jps -l
47172 portal
6120 sun.tools.jps.jps
c:program filesjavajdk1.8.0_171bin>
在linux上
[geekfkare@localhost ~]# jps -l
1088 /opt/keycloak/jboss-modules.jar
26680 /var/lib/jenkins/workspace/kyc/kyc/target/kyc-1.0.jar
7193 jdk.jcmd/sun.tools.jps.jps
2058 /usr/share/jenkins/jenkins.war
11933 /var/lib/jenkins/workspace/admin-portal/target/portal-1.0.jar
[geekfkare@localhost ~]#
正如我们在这里所看到的,我们得到了一个正在运行的java进程列表。在第一和第二列中分别包含了运行中java进程的本地vm id和应用程序名称。现在,为了生成线程转储,我们使用jstack程序和-l标志,它会创建一个长列表形式的转储输出。我们还可以将输出导入到我们选择的某个文本文件中。
jstack -l 26680
[geekfkare@localhost ~]# jstack -l 26680
2020-06-27 06:04:53
完整线程转储 java hotspot(tm) 64-bit server vm (25.221-b11 mixed mode):
"attach listener"#16287守护进程prio=9 os_prio=0 tid=0x00007f0814001800 nid=0x4ff2等待条件【0x0000000000000000】
java.lang.thread.state: runnable
已锁定的可拥有同步器:
- 无
"logback-8"#2316守护进程prio=5 os_prio=0 tid=0x00007f07e0033000 nid=0x4792等待条件【0x00007f07baff8000】
java.lang.thread.state: waiting (parking)
at sun.misc.unsafe.park(native method)
- 正在等待 (一个java.util.concurrent.locks.abstractqueuedsynchronizer$conditionobject)
at java.util.concurrent.locks.locksupport.park(locksupport.java:175)
at java.util.concurrent.locks.abstractqueuedsynchronizer$conditionobject.await(abstractqueuedsynchronizer.java:2039)
at java.util.concurrent.scheduledthreadpoolexecutor$delayedworkqueue.take(scheduledthreadpoolexecutor.java:1081)
at java.util.concurrent.scheduledthreadpoolexecutor$delayedworkqueue.take(scheduledthreadpoolexecutor.java:809)
at java.util.concurrent.threadpoolexecutor.gettask(threadpoolexecutor.java:1074)
at java.util.concurrent.threadpoolexecutor.runworker(threadpoolexecutor.java:1134)
at java.util.concurrent.threadpoolexecutor$worker.run(threadpoolexecutor.java:624)
at java.lang.thread.run(thread.java:748)
已锁定的可拥有同步器:
- 无
"logback-7"#2315守护进程prio=5 os_prio=0 tid=0x00007f07e0251800 nid=0x4791等待条件【0x00007f07bb0f9000】
java.lang.thread.state: waiting (parking)
at sun.misc.unsafe.park(native method)
- 正在等待 (一个java.util.concurrent.locks.abstractqueuedsynchronizer$conditionobject)
at java.util.concurrent.locks.locksupport.park(locksupport.java:175)
at java.util.concurrent.locks.abstractqueuedsynchronizer$conditionobject.await(abstractqueuedsynchronizer.java:2039)
at java.util.concurrent.scheduledthreadpoolexecutor$delayedworkqueue.take(scheduledthreadpoolexecutor.java:1081)
at java.util.concurrent.scheduledthreadpoolexecutor$delayedworkqueue.take(scheduledthreadpoolexecutor.java:809)
at java.util.concurrent.threadpoolexecutor.gettask(threadpoolexecutor.java:1074)
at java.util.concurrent.threadpoolexecutor.runworker(threadpoolexecutor.java:1134)
at java.util.concurrent.threadpoolexecutor$worker.run(threadpoolexecutor.java:624)
at java.lang.thread.run(thread.java:748)
已锁定的可拥有同步器:
- 无
#2. jvisualvm
jvisualvm 是一个gui工具,帮助我们排除故障,监视和分析java应用程序。它还带有jvm,并可以从我们的java安装目录的/bin目录中启动。它非常直观和易于使用。除其他选项外,它还允许我们捕获特定进程的线程转储。
要查看特定进程的线程转储,我们可以右键单击程序,然后从上下文菜单中选择thread dump。
#3. jcmd
jcmd 是一个命令行实用程序,随jdk一起提供,用于向jvm发送诊断命令请求。
但它只能在运行java应用程序的本地机器上运行。它可以用于控制java flight recordings,诊断和排除故障jvm和java应用程序。我们可以使用jcmd的thread.print
命令来获取由pid指定的特定进程的线程转储列表。
下面是如何使用jcmd
的示例。
jcmd 28036 thread.print
c:program filesjavajdk1.8.0_171bin>jcmd 28036 thread.print
28036:
2020-06-27 21:20:02
完整线程转储 java hotspot(tm) 64-bit server vm (25.171-b11 mixed mode):
“bundle file closer” #14 daemon prio=5 os_prio=0 tid=0x0000000021d1c000 nid=0x1d4c in object.wait() [0x00000000244ef000]
java.lang.thread.state: waiting (on object monitor)
at java.lang.object.wait(native method)
at java.lang.object.wait(unknown source)
at org.eclipse.osgi.framework.eventmgr.eventmanager$eventthread.getnextevent(eventmanager.java:403)
– locked (a org.eclipse.osgi.framework.eventmgr.eventmanager$eventthread)
at org.eclipse.osgi.framework.eventmgr.eventmanager$eventthread.run(eventmanager.java:339)
“active thread: equinox container: 0b6cc851-96cd-46de-a92b-253c7f7671b9” #12 prio=5 os_prio=0 tid=0x0000000022e61800 nid=0xbff4 waiting on condition [0x00000000243ee000]
java.lang.thread.state: timed_waiting (parking)
at sun.misc.unsafe.park(native method)
– parking to wait for (a java.util.concurrent.locks.abstractqueuedsynchronizer$conditionobject)
at java.util.concurrent.locks.locksupport.parknanos(unknown source)
at java.util.concurrent.locks.abstractqueuedsynchronizer$conditionobject.awaitnanos(unknown source)
at java.util.concurrent.scheduledthreadpoolexecutor$delayedworkqueue.take(unknown source)
at java.util.concurrent.scheduledthreadpoolexecutor$delayedworkqueue.take(unknown source)
at java.util.concurrent.threadpoolexecutor.gettask(unknown source)
at java.util.concurrent.threadpoolexecutor.runworker(unknown source)
at java.util.concurrent.threadpoolexecutor$worker.run(unknown source)
at java.lang.thread.run(unknown source)
“service thread” #10 daemon prio=9 os_prio=0 tid=0x0000000021a7b000 nid=0x2184 runnable [0x0000000000000000] java.lang.thread.state: runnable
“c1 compilerthread3” #9 daemon prio=9 os_prio=2 tid=0x00000000219f5000 nid=0x1300 waiting on condition [0x0000000000000000] java.lang.thread.state: runnable
“c2 compilerthread2” #8 daemon prio=9 os_prio=2 tid=0x00000000219e0000 nid=0x48f4 waiting on condition [0x0000000000000000] java.lang.thread.state: runnable
“c2 compilerthread1” #7 daemon prio=9 os_prio=2 tid=0x00000000219df000 nid=0xb314 waiting on condition [0x0000000000000000] java.lang.thread.state: runnable
“c2 compilerthread0” #6 daemon prio=9 os_prio=2 tid=0x00000000219db800 nid=0x2260 waiting on condition [0x0000000000000000] java.lang.thread.state: runnable
“attach listener” #5 daemon prio=5 os_prio=2 tid=0x00000000219d9000 nid=0x125c waiting on condition [0x0000000000000000] java.lang.thread.state: runnable
“signal dispatcher” #4 daemon prio=9 os_prio=2 tid=0x00000000219d8000 nid=0x834 runnable [0x0000000000000000] java.lang.thread.state: runnable
“finalizer” #3 daemon prio=8 os_prio=1 tid=0x000000001faf3000 nid=0x36c0 in object.wait() [0x0000000021eae000]
java.lang.thread.state: waiting (on object monitor)
at java.lang.object.wait(native method)
– waiting on (a java.lang.ref.referencequeue$lock)
at java.lang.ref.referencequeue.remove(unknown source)
– locked (a java.lang.ref.referencequeue$lock)
at java.lang.ref.referencequeue.remove(unknown source)
at java.lang.ref.finalizer$finalizerthread.run(unknown source)
“reference handler” #2 daemon prio=10 os_prio=2 tid=0x0000000005806000 nid=0x13c0 in object.wait() [0x00000000219af000]
java.lang.thread.state: waiting (on object monitor)
at java.lang.object.wait(native method)
– waiting on (a java.lang.ref.reference$lock)
at java.lang.object.wait(unknown source)
at java.lang.ref.reference.tryhandlepending(unknown source)
– locked (a java.lang.ref.reference$lock)
at java.lang.ref.reference$referencehandler.run(unknown source)
“main” #1 prio=5 os_prio=0 tid=0x000000000570e800 nid=0xbf8 runnable [0x0000000000fec000]
java.lang.thread.state: runnable
at java.util.zip.zipfile.open(native method)
at java.util.zip.zipfile.(unknown source)
at java.util.zip.zipfile.(unknown source)
at java.util.zip.zipfile.(unknown source)
at org.eclipse.osgi.framework.util.secureaction.getzipfile(secureaction.java:307)
at org.eclipse.osgi.storage.bundlefile.zipbundlefile.getzipfile(zipbundlefile.java:136)
at org.eclipse.osgi.storage.bundlefile.zipbundlefile.lockopen(zipbundlefile.java:83)
at org.eclipse.osgi.storage.bundlefile.zipbundlefile.getentry(zipbundlefile.java:290)
at org.eclipse.equinox.weaving.hooks.weavingbundlefile.getentry(weavingbundlefile.java:65)
at org.eclipse.osgi.storage.bundlefile.bundlefilewrapper.getentry(bundlefilewrapper.java:55)
at org.eclipse.osgi.storage.bundleinfo$generation.getrawheaders(bundleinfo.java:130)
– locked (a java.lang.object)
at org.eclipse.osgi.storage.bundleinfo$cachedmanifest.get(bundleinfo.java:599)
at org.eclipse.osgi.storage.bundleinfo$cachedmanifest.get(bundleinfo.java:1)
at org.eclipse.equinox.weaving.hooks.supplementerregistry.addsupplementer(supplementerregistry.java:172)
at org.eclipse.equinox.weaving.hooks.weavinghook.initialize(weavinghook.java:138)
at org.eclipse.equinox.weaving.hooks.weavinghook.start(weavinghook.java:208)
at org.eclipse.osgi.storage.frameworkextensioninstaller.startactivator(frameworkextensioninstaller.java:261)
at org.eclipse.osgi.storage.frameworkextensioninstaller.startextensionactivators(frameworkextensioninstaller.java:198)
at org.eclipse.osgi.internal.framework.systembundleactivator.start(systembundleactivator.java:112)
at org.eclipse.osgi.internal.framework.bundlecontextimpl$3.run(bundlecontextimpl.java:815)
at org.eclipse.osgi.internal.framework.bundlecontextimpl$3.run(bundlecontextimpl.java:1)
at java.security.accesscontroller.doprivileged(native method)
at org.eclipse.osgi.internal.framework.bundlecontextimpl.startactivator(bundlecontextimpl.java:808)
at org.eclipse.osgi.internal.framework.bundlecontextimpl.start(bundlecontextimpl.java:765)
at org.eclipse.osgi.internal.framework.equinoxbundle.startworker0(equinoxbundle.java:1005)
at org.eclipse.osgi.internal.framework.equinoxbundle$systembundle$equinoxsystemmodule.initworker(equinoxbundle.java:190)
at org.eclipse.osgi.container
#4. jmc
jmc代表java任务控制。它是一种与jdk捆绑在一起的开源gui工具,用于收集和分析java应用程序的数据。
它可以从java安装的/bin文件夹中启动。java管理员和开发人员使用该工具收集有关jvm和应用程序行为的详细低级信息。它可以对javaflight recorder收集的数据进行详细且高效的分析。
在启动jmc
后,我们可以看到正在本地计算机上运行的java进程列表。也可以进行远程连接。在特定进程上,我们可以右键单击并选择开始飞行记录,然后在线程选项卡中检查线程转储。
#5. jconsole
jconsole是一种用于投诉管理和监控的java管理扩展工具。
它还具有对jmx代理的一组预定义操作,用户可以执行这些操作。它可以从java安装的/bin文件夹中启动。
使用jconsolegui工具,我们可以在将其连接到正在运行的java进程时检查每个线程的堆栈跟踪。然后,在线程选项卡中,我们可以看到所有运行线程的名称。要检测死锁,我们可以点击窗口右下角的检测死锁。如果检测到死锁,它将显示在一个新的选项卡中,否则将显示未检测到死锁。
#6. threadmxbean
threadmxbean是属于java.lang.management包的java虚拟机线程系统管理的接口。它主要用于检测进入死锁状态的线程并获取有关它们的详细信息。
我们可以使用threadmxbean接口以编程方式捕获线程转储。使用managementfactory
的getthreadmxbean()
方法来获取threadmxbean
接口的实例。它返回守护线程和非守护线程的数量。managementfactory是一个用于获取java平台的托管bean的工厂类。
private static string getthreaddump (boolean lockmonitors, boolean locksynchronizers) {
stringbuffer threaddump = new stringbuffer (system.lineseparator ());
threadmxbean threadmxbean = managementfactory.getthreadmxbean ();
for (threadinfo threadinfo : threadmxbean.dumpallthreads (lockmonitors, locksynchronizers)) {
threaddump.append (threadinfo.tostring ());
}
return threaddump.tostring ();
}
线程转储的手动分析
通过分析线程转储,可以准确定位多线程进程中的问题。可以通过可视化每个线程转储的状态来解决死锁、锁争用和单个线程转储的过多cpu利用率等问题。
通过分析线程转储后,可以通过纠正每个线程的状态来实现应用程序的最大吞吐量。
例如,假设一个进程正在使用大量的cpu,我们可以找出是否有任何线程正在使用最多的cpu。如果有这样的线程,我们将其lwp数字转换为十六进制数字。然后从线程转储中,我们可以找到具有nid等于先前获取的十六进制数字的线程。使用线程的堆栈跟踪,我们可以确定问题所在。使用以下命令找出线程的进程id。
ps -mo pid,lwp,stime,time,cpu -c java
[geekfkare@localhost ~]# ps -mo pid,lwp,stime,time,cpu -c java
pid lwp stime time %cpu
26680 - dec07 00:02:02 99.5
- 10039 dec07 00:00:00 0.1
- 10040 dec07 00:00:00 95.5
让我们来看下面一段线程转储。要获取进程26680的线程转储,请使用jstack -l 26680
[geekfkare@localhost ~]# jstack -l 26680
2020-06-27 09:01:29
full thread dump java hotspot(tm) 64-bit server vm (25.221-b11 mixed mode):
"attach listener" #16287 daemon prio=9 os_prio=0 tid=0x00007f0814001800 nid=0x4ff2 waiting on condition [0x0000000000000000]
java.lang.thread.state: runnable
locked ownable synchronizers:
- none
.
.
.
.
.
.
.
"reference handler" #2 daemon prio=10 os_prio=0 tid=0x00007f085814a000 nid=0x6840 in object.wait() [0x00007f083b2f1000]
java.lang.thread.state: waiting (on object monitor)
at java.lang.object.wait(native method)
at java.lang.object.wait(object.java:502)
at java.lang.ref.reference.tryhandlepending(reference.java:191)
- locked (a java.lang.ref.reference$lock)
at java.lang.ref.reference$referencehandler.run(reference.java:153)
locked ownable synchronizers:
- none
"vm thread" os_prio=0 tid=0x00007f0858140800 nid=0x683f runnable
"gc task thread#0 (parallelgc)" os_prio=0 tid=0x00007f0858021000 nid=0x683b runnable
"gc task thread#1 (parallelgc)" os_prio=0 tid=0x00007f0858022800 nid=0x683c runnable
"gc task thread#2 (parallelgc)" os_prio=0 tid=0x00007f0858024800 nid=0x683d runnable
"gc task thread#3 (parallelgc)" os_prio=0 tid=0x00007f0858026000 nid=0x683e runnable
"vm periodic task thread" os_prio=0 tid=0x00007f08581a0000 nid=0x6847 waiting on condition
jni global references: 1553
现在,让我们看看可以使用线程转储探索哪些内容。如果我们观察线程转储,我们会看到很多内容,可能会让人感到不知所措。然而,如果我们一步一步来,理解起来就会相当简单。让我们先理解第一行
2020-06-27 09:01:29
full thread dump java hotspot(tm) 64-bit server vm (25.221-b11 mixed mode):
以上显示了转储生成的时间以及使用的jvm的信息。接下来,在最后,我们可以看到线程列表,其中第一个是我们的reference handler线程。
分析阻塞的线程
如果我们分析下面的线程转储日志,我们可以发现它检测到了状态为blocked的线程,这会导致应用程序的性能非常慢。因此,如果我们能找到blocked的线程,我们可以尝试提取与线程正在尝试获取的锁相关的线程。分析当前持有锁的线程的堆栈跟踪可以帮助解决问题。
[geekfkare@localhost ~]# jstack -l 26680
.
.
.
.
" 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 (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 (a beans.connectionpool)
at beans.cus.servicecnt.gettodaycount(servicecnt.java:111)
at beans.cus.servicecnt.insertcount(servicecnt.java:43)
.
.
.
.
分析死锁线程
线程转储的另一个常见应用是检测死锁。如果我们分析线程转储,检测和解决死锁问题会更加容易。
死锁是至少涉及两个线程的情况,其中一个线程需要继续执行的资源被另一个线程锁定,同时第二个线程需要的资源被第一个线程锁定。
因此,没有一个线程可以继续执行,这导致了死锁情况,并导致应用程序被卡住。如果存在死锁,则线程转储的最后一部分将打印有关死锁的信息,如下所示。
"thread-0":
waiting to lock monitor 0x00000250e4982480 (object 0x00000000894465b0, a java.lang.object),
which is held by "thread-1"
"thread-1":
waiting to lock monitor 0x00000250e4982380 (object 0x00000000894465a0, a java.lang.object),
which is held by "thread-0"
.
.
.
"thread-0":
at deadlockedprogram$deadlockedrunnableimplementation.run(deadlockedprogram.java:34)
- waiting to lock (a java.lang.object)
- locked (a java.lang.object)
at java.lang.thread.run([email protected]/thread.java:844)
"thread-1":
at deadlockedprogram $deadlockrunnableimplementation.run(deadlockedprogram.java:34)
- waiting to lock (a java.lang.object)
- locked (a java.lang.object)
at java.lang.thread.run([email protected]/thread.java:844)
在这里,我们可以以相当容易阅读的格式看到死锁信息。
除此之外,如果我们将上述线程转储的所有片段汇总起来,那么它会给出以下信息。
- reference handler是线程的可读名称。
- #2是线程的唯一id。
- daemon表示线程是否为守护线程。
- 线程的数字优先级由prio=10给出。
- 线程的当前状态由waiting on condition表示。
- 然后我们可以看到堆栈跟踪,其中包括锁定信息。
线程转储分析工具
除了手动分析外,有许多在线和离线工具可用于分析线程转储。以下是一些列出的工具,我们可以根据需求使用。
首先,让我们探索在线工具。
#1. fast thread
fast thread是devops工程师最喜欢的线程转储分析工具,用于解决复杂的生产问题。这是一个在线的java线程转储分析工具,我们可以将线程转储上传为文件,也可以直接复制和粘贴线程转储。
根据大小,它将分析线程转储并显示如屏幕截图所示的信息。
特点
- 解决jvm崩溃、减速、内存泄漏、冻结、cpu峰值等问题
- 即时rca(不必等待供应商)
- 直观的仪表板
- rest api支持
- 机器学习
#2. spotify线程转储分析器
spotify线程转储分析器在apache许可证的2.0版本下获得许可。它是一个在线工具,可以将线程转储作为文件接受,也可以直接复制和粘贴线程转储。根据大小,它将分析线程转储并显示如屏幕截图所示的信息。
#3. jstack review
jstack.review从浏览器内部分析java线程转储。此页仅限客户端。
#4. site 24×7
这个工具是检测降低java虚拟机(jvm)性能的故障线程的先决条件。通过可视化各个线程转储的状态,可以解决死锁、锁争用和个别线程转储的过多cpu利用率等问题。
通过纠正工具提供的每个线程的状态,可以实现应用程序的最大吞吐量。
现在,让我们探索离线工具。
在性能分析方面,只有最好的工具才够好。
#1. jprofiler
jprofiler是java开发人员中最受欢迎的线程转储分析工具之一。jprofiler直观的ui帮助您解决性能瓶颈,确定内存泄漏并理解线程问题。
jprofiler支持以下平台的性能分析:
- windows
- macos
- linux
- freebsd
- solaris
- aix
- hp-ux
下面是jprofiler成为在jvm上分析我们的应用程序的首选工具的一些特点。
特点
- 为jdbc、jpa和nosql提供数据库分析支持
- 还提供对java企业版的支持
- 提供关于rmi调用的高级信息
- 对内存泄漏进行详细分析
- 具备广泛的qa能力
- 集成的线程分析器与cpu分析视图紧密结合
- 支持平台、ide和应用服务器
#2. ibm tmda
ibm thread and monitor dump analyzer for java(tmda)是一种工具,可以在java线程转储中识别挂起、死锁、资源争用和瓶颈。这是ibm的产品,但是tmda工具是无任何保修或支持的;然而,他们会不断修复和增强该工具。
#3. manageengine
manageengine应用程序管理器可以帮助监控jvm堆和非堆内存。我们甚至可以配置阈值,并通过电子邮件、短信等方式接收警报,以确保java应用程序的调优。
#4. yourkit
yourkit包括以下产品,称之为一个”kit”。
- java profiler – 专为java ee和java se平台提供的功能齐全且开销低的分析器。
- youmonitor – 对jenkins、teamcity、gradle、maven、ant、junit和testng的性能监控和分析。
- .net profiler – 用于.net框架的易于使用的性能和内存分析器。
结论
现在您知道了,在理解和诊断多线程应用程序中的问题时,线程转储是如何有用的。通过适当的知识,了解线程转储的结构、其中包含的信息等等,我们可以利用它们快速识别问题的原因。