深入Java多线程编程之读取线程的dump信息

今天开发的大多数Java应用程序涉及多个线程,在享受其优点的同时,它也带来了许多小小的问题。在单线程应用程序中,所有资源(共享数据,输入/输出(IO)设备等)都可以无需协调地访问,因为在应用程序内执行的单个线程是任何给定时间利用资源的唯一线程。

在多线程应用程序的情况下,需要进行折衷 - 增加复杂性以提高性能,其中多个线程可以利用可用的(通常多于一个)中央处理单元(CPU)内核。在正确的条件下,应用程序可以看到使用多线程(由Amdahl法则形式化)显着提高性能,但必须特别注意确保多个线程在访问两个线程所需的资源时正确协调。在很多情况下,像Spring这样的框架,将抽象直接线程管理,但即使使用这些抽象线程也会导致一些难以调试的问题。考虑到所有这些困难,最终可能会出现问题,作为开发人员,我们必须开始诊断线程的不确定领域。

幸运的是,Java有一种机制来检查应用程序中所有线程的状态 - Thread Dump。在本文中,我们将看看Thread Dump的重要性,以及如何解密其文件格式,以及如何在实际应用程序中生成和分析Thread Dump。本文假定读者对线程以及围绕线程的各种问题(包括线程争用和共享资源管理)有基本的了解。即使有了这样的理解,在生成和检查Thread Dump之前,重要的是要巩固一些中心线程术语。

理解术语

JavaThread Dump起初可能看起来很神秘,但要理解Thread Dump需要理解一些基本术语。通常,以下术语是掌握JavaThread Dump的含义和上下文的关键:

  • 线程 - 由Java虚拟机(JVM)管理的离散并发单元。线程映射到操作系统(OS)线程,称为本地线程,它提供执行指令(代码)的机制。每个线程有一个唯一的标识符--名称,并且可以被分类为守护线程非守护线程,其中守护线程区别其他线程独立运行于系统中,并且仅当Runtime.exit 方法被调用(以及安全管理器授权退出程序)或者所有非守护线程都已经销毁的时候才会被销毁。有关更多信息,请参阅Thread 类文档
  • 活动的线程 -正在执行一些工作的正在运行的线程(正常的 线程状态)。
  • 被阻塞的线程 - 一个尝试进入同步块的线程,但另一个线程已经锁定了同一个同步块。
  • 等待线程 - 一个调用wait 方法(有可能超时)的线程,目前正在等待另一个线程调用 同一对象上的notify 方法(或 notifyAll)。请注意,如果线程wait 在具有超时的对象上调用方法,并且指定的超时已过期,则不认为线程正在等待 。
  • 睡眠线程 - 由于调用Thread.sleep 方法(具有指定的睡眠长度)而当前不执行的线程 。
  • 监视器 - JVM使用的一种机制,以便于并发访问单个对象。这种机制是通过使用 synchronized 关键字来实现的,其中Java中的每个对象都有一个关联的监视器,允许任何线程同步或锁定一个对象,以确保在锁被释放之前没有其他线程访问锁定对象(同步块已退出) 。有关更多信息,请参阅Java语言规范(JLS)同步部分(17.1)
  • 死锁 -一个线程持有某个资源A并被阻止的情况,等待某个资源B变为可用,而另一个线程持有资源B 并被阻止,从而等待资源A 变为可用状态。发生死锁时,程序中不会有进展。需要注意的是,死锁也可能发生在两个以上的线程中,其中三个或更多线程全部保存另一个线程所需的资源并同时被阻塞,等待另一个线程占用的资源。当某个线程X拥有资源A 并且需要资源C,线程Y持有资源B 并且需要资源A,会发生这种情况并且线程Z 持有资源C 并且需要资源B (正式地称为用餐哲学家问题)。

  • 活锁 - 一种场景,其中线程A 执行的动作会导致线程B 执行一个动作,从而导致线程A 执行其原始动作。这种情况可以看作是一只追逐尾巴的狗。与死锁类似,实时锁定线程不会取得进展,但与死锁不同,线程不会被阻塞(而是处于活动状态)。

上述定义并不构成Java线程或Thread Dump的全面词汇表,而是构成读取典型Thread Dump时所经历的大部分术语。有关Java线程和Thread Dump的更详细词汇,请参见JLSJava并发实践的第17节

通过对Java线程的基本理解,我们可以开始创建一个应用程序,从中我们将生成一个Thread Dump,然后检查Thread Dump的关键部分,以获取有关程序中线程的有用信息。

创建一个示例程序

为了生成Thread Dump,我们需要先执行一个Java应用程序。虽然一个简单的“hello, world!” 应用程序导致过于简单的Thread Dump,即使是中等大小的多线程应用程序中的Thread Dump也可能令人难以置信。为了理解Thread Dump的基础知识,我们将使用以下程序,该程序启动两个最终会死锁的线程:

public class DeadlockProgram {
 public static void main(String[] args) throws Exception {
 Object resourceA = new Object();
 Object resourceB = new Object();
 Thread threadLockingResourceAFirst = new Thread(new DeadlockRunnable(resourceA, resourceB));
 Thread threadLockingResourceBFirst = new Thread(new DeadlockRunnable(resourceB, resourceA));
 threadLockingResourceAFirst.start();
 Thread.sleep(500);
 threadLockingResourceBFirst.start();
 }
 private static class DeadlockRunnable implements Runnable {
 private final Object firstResource;
 private final Object secondResource;
 public DeadlockRunnable(Object firstResource, Object secondResource) {
 this.firstResource = firstResource;
 this.secondResource = secondResource;
 }
 @Override
 public void run() {
 try {
 synchronized(firstResource) {
 printLockedResource(firstResource);
 Thread.sleep(1000);
 synchronized(secondResource) {
 printLockedResource(secondResource);
 }
 }
 } catch (InterruptedException e) {
 System.out.println("Exception occurred: " + e);
 }
 }
 private static void printLockedResource(Object resource) {
 System.out.println(Thread.currentThread().getName() + ": locked resource -> " + resource);
 }
 }
}

这个程序只是创建两个资源, resourceA并且resourceB,启动两个线程,threadLockingResourceAFirst并且threadLockingResourceBFirst锁定这些资源中的每一个。导致死锁的关键是确保threadLockingResourceAFirst尝试锁定resourceA然后锁定 resourceB而threadLockingResourceBFirst尝试锁定resourceB然后resourceA。延迟添加使线程threadLockingResourceAFirstsleeps有能力锁定resourceB前,线程threadLockingResourceBFirst有足够的时间锁定resourceB,注意上面两个sleep,一个500,一个1000,时间差就是这么形成的。当两个线程等待时,他们发现他们想要的第二个资源已经被锁定,并且两个线程都被阻塞,等待另一个线程放弃其锁定的资源(可是永远不会发生)。

执行此程序会产生以下输出,其中对象哈希(数字后面的java.lang.Object@)在每次执行之间会有所不同:

Thread-0: locked resource -> java.lang.Object@149bc794
Thread-1: locked resource -> java.lang.Object@17c10009

在输出完成时,程序看起来好像正在运行(执行该程序的进程没有终止),但没有进一步的工作正在完成。这是实践中的僵局。为了解决手头的问题,我们必须手动生成Thread Dump并检查转储中线程的状态。

生成Thread Dump

实际上,Java程序可能会异常终止并自动生成Thread Dump,但在某些情况下(例如多死锁),程序不会终止,但会显示为卡住。要为此卡住的程序生成Thread Dump,我们必须首先发现程序的进程ID(PID)。为此,我们使用所有Java开发工具包(JDK)7+安装中包含的JVM进程状态(JPS)工具。为了找到我们的死锁程序的PID,我们只需 jps 在终端(Windows或Linux)中执行:

$ jps
11568 DeadlockProgram
15584 Jps
15636

第一列表示正在运行的Java进程的本地VM ID(lvmid)。在本地JVM的上下文中,lvmid映射到Java进程的PID。请注意,此值可能与上面的值不同。第二列表示应用程序的名称,可以映射到主类的名称,Java归档(JAR)文件或 Unknown取决于程序运行的特征。

在我们的例子中,应用程序名称DeadlockProgram与我们的程序启动时执行的主类文件的名称相匹配。在上面的例子中,我们程序的PID是11568,它为我们提供了足够的信息来生成Thread Dump。为了生成thread dump,我们使用该jstack 程序(包含在所有JDK 7+安装中),提供 -l 标志(创建一个长列表)和我们的死锁程序的PID,将输出生成到文本文件(即thread_dump.txt):

jstack -l 11568> thread_dump.txt

该 thread_dump.txt 文件现在包含我们死锁程序的Thread Dump,并包含一些非常有用的信息,用于诊断死锁问题的根本原因。请注意,如果我们没有安装JDK 7+,我们也可以通过用SIGQUIT 信号退出死锁程序来生成Thread Dump 。要在Linux上执行此操作,只需使用PID(11568 在我们的示例中)以及 -3 标志杀死死锁程序:

kill -3 11568

读一个简单的Thread Dump

打开 thread_dump.txt 文件,我们看到它包含以下内容:

2018-06-19 16:44:44
Full thread dump Java HotSpot(TM) 64-Bit Server VM (10.0.1+10 mixed mode):
Threads class SMR info:
_java_thread_list=0x00000250e5488a00, length=13, elements={
0x00000250e4979000, 0x00000250e4982800, 0x00000250e52f2800, 0x00000250e4992800,
0x00000250e4995800, 0x00000250e49a5800, 0x00000250e49ae800, 0x00000250e5324000,
0x00000250e54cd800, 0x00000250e54cf000, 0x00000250e54d1800, 0x00000250e54d2000,
0x00000250e54d0800
}
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x00000250e4979000 nid=0x3c28 waiting on condition [0x000000b82a9ff000]
 java.lang.Thread.State: RUNNABLE
 at java.lang.ref.Reference.waitForReferencePendingList(java.base@10.0.1/Native Method)
 at java.lang.ref.Reference.processPendingReferences(java.base@10.0.1/Reference.java:174)
 at java.lang.ref.Reference.access$000(java.base@10.0.1/Reference.java:44)
 at java.lang.ref.Reference$ReferenceHandler.run(java.base@10.0.1/Reference.java:138)
 Locked ownable synchronizers:
 - None
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000250e4982800 nid=0x2a54 in Object.wait() [0x000000b82aaff000]
 java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(java.base@10.0.1/Native Method)
 - waiting on <0x0000000089509410> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(java.base@10.0.1/ReferenceQueue.java:151)
 - waiting to re-lock in wait() <0x0000000089509410> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(java.base@10.0.1/ReferenceQueue.java:172)
 at java.lang.ref.Finalizer$FinalizerThread.run(java.base@10.0.1/Finalizer.java:216)
 Locked ownable synchronizers:
 - None
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x00000250e52f2800 nid=0x2184 runnable [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 Locked ownable synchronizers:
 - None
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x00000250e4992800 nid=0x1624 waiting on condition [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 Locked ownable synchronizers:
 - None
"C2 CompilerThread0" #6 daemon prio=9 os_prio=2 tid=0x00000250e4995800 nid=0x4198 waiting on condition [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 No compile task
 Locked ownable synchronizers:
 - None
"C2 CompilerThread1" #7 daemon prio=9 os_prio=2 tid=0x00000250e49a5800 nid=0x3b98 waiting on condition [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 No compile task
 Locked ownable synchronizers:
 - None
"C1 CompilerThread2" #8 daemon prio=9 os_prio=2 tid=0x00000250e49ae800 nid=0x1a84 waiting on condition [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 No compile task
 Locked ownable synchronizers:
 - None
"Sweeper thread" #9 daemon prio=9 os_prio=2 tid=0x00000250e5324000 nid=0x5f0 runnable [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 Locked ownable synchronizers:
 - None
"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x00000250e54cd800 nid=0x169c runnable [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 Locked ownable synchronizers:
 - None
"Common-Cleaner" #11 daemon prio=8 os_prio=1 tid=0x00000250e54cf000 nid=0x1610 in Object.wait() [0x000000b82b2fe000]
 java.lang.Thread.State: TIMED_WAITING (on object monitor)
 at java.lang.Object.wait(java.base@10.0.1/Native Method)
 - waiting on <0x000000008943e600> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(java.base@10.0.1/ReferenceQueue.java:151)
 - waiting to re-lock in wait() <0x000000008943e600> (a java.lang.ref.ReferenceQueue$Lock)
 at jdk.internal.ref.CleanerImpl.run(java.base@10.0.1/CleanerImpl.java:148)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
 at jdk.internal.misc.InnocuousThread.run(java.base@10.0.1/InnocuousThread.java:134)
 Locked ownable synchronizers:
 - None
"Thread-0" #12 prio=5 os_prio=0 tid=0x00000250e54d1800 nid=0xdec waiting for monitor entry [0x000000b82b4ff000]
 java.lang.Thread.State: BLOCKED (on object monitor)
 at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34)
 - waiting to lock <0x00000000894465b0> (a java.lang.Object)
 - locked <0x00000000894465a0> (a java.lang.Object)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
 Locked ownable synchronizers:
 - None
"Thread-1" #13 prio=5 os_prio=0 tid=0x00000250e54d2000 nid=0x415c waiting for monitor entry [0x000000b82b5ff000]
 java.lang.Thread.State: BLOCKED (on object monitor)
 at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34)
 - waiting to lock <0x00000000894465a0> (a java.lang.Object)
 - locked <0x00000000894465b0> (a java.lang.Object)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
 Locked ownable synchronizers:
 - None
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x00000250e54d0800 nid=0x2b8c waiting on condition [0x0000000000000000]
 java.lang.Thread.State: RUNNABLE
 Locked ownable synchronizers:
 - None
"VM Thread" os_prio=2 tid=0x00000250e496d800 nid=0x1920 runnable 
"GC Thread#0" os_prio=2 tid=0x00000250c35b5800 nid=0x310c runnable 
"GC Thread#1" os_prio=2 tid=0x00000250c35b8000 nid=0x12b4 runnable 
"GC Thread#2" os_prio=2 tid=0x00000250c35ba800 nid=0x43f8 runnable 
"GC Thread#3" os_prio=2 tid=0x00000250c35c0800 nid=0x20c0 runnable 
"G1 Main Marker" os_prio=2 tid=0x00000250c3633000 nid=0x4068 runnable 
"G1 Conc#0" os_prio=2 tid=0x00000250c3636000 nid=0x3e28 runnable 
"G1 Refine#0" os_prio=2 tid=0x00000250c367e000 nid=0x3c0c runnable 
"G1 Refine#1" os_prio=2 tid=0x00000250e47fb800 nid=0x3890 runnable 
"G1 Refine#2" os_prio=2 tid=0x00000250e47fc000 nid=0x32a8 runnable 
"G1 Refine#3" os_prio=2 tid=0x00000250e47fd800 nid=0x3d00 runnable 
"G1 Young RemSet Sampling" os_prio=2 tid=0x00000250e4800800 nid=0xef4 runnable 
"VM Periodic Task Thread" os_prio=2 tid=0x00000250e54d6800 nid=0x3468 waiting on condition 
JNI global references: 2
Found one Java-level deadlock:
=============================
"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"
Java stack information for the threads listed above:
===================================================
"Thread-0":
 at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34)
 - waiting to lock <0x00000000894465b0> (a java.lang.Object)
 - locked <0x00000000894465a0> (a java.lang.Object)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
"Thread-1":
 at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34)
 - waiting to lock <0x00000000894465a0> (a java.lang.Object)
 - locked <0x00000000894465b0> (a java.lang.Object)
 at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
Found 1 deadlock.

介绍性信息

虽然这个文件起初看到会一头雾水,但如果我们一步一步地分析,实际上很简单。第一行显示生成转储的时间戳,而第二行包含有关生成转储的JVM的诊断信息:

2018-06-19 16:44:44

Full thread dump Java HotSpot(TM) 64-Bit Server VM (10.0.1+10 mixed mode):

虽然这些行不提供关于我们系统中的线程的任何信息,但它们提供了一个上下文,从中可以构建转储的其余部分(即,哪个JVM生成转储以及转储何时生成)。

通用线程信息

下一节开始向我们提供一些有关在Thread Dump时正在运行的线程的有用信息:

Threads class SMR info:

_java_thread_list=0x00000250e5488a00, length=13, elements={

0x00000250e4979000, 0x00000250e4982800, 0x00000250e52f2800, 0x00000250e4992800,

0x00000250e4995800, 0x00000250e49a5800, 0x00000250e49ae800, 0x00000250e5324000,

0x00000250e54cd800, 0x00000250e54cf000, 0x00000250e54d1800, 0x00000250e54d2000,

0x00000250e54d0800

}

本部分包含线程列表安全内存回收(SMR)信息1,其中列举了所有非JVM内部线程(例如,非VM和非垃圾收集(GC))的地址。如果我们检查这些地址,我们可以看到它们对应于 tid的值----本地线程对象地址,而不是Thread ID,正如我们很快会看到的那样 -转储中每个编号线程的值(这里,省略号用于隐藏多余的信息):

"Reference Handler" #2 ... tid=0x00000250e4979000 ...

"Finalizer" #3 ... tid=0x00000250e4982800 ...

"Signal Dispatcher" #4 ... tid=0x00000250e52f2800 ...

"Attach Listener" #5 ... tid=0x00000250e4992800 ...

"C2 CompilerThread0" #6 ... tid=0x00000250e4995800 ...

"C2 CompilerThread1" #7 ... tid=0x00000250e49a5800 ...

"C1 CompilerThread2" #8 ... tid=0x00000250e49ae800 ...

"Sweeper thread" #9 ... tid=0x00000250e5324000 ...

"Service Thread" #10 ... tid=0x00000250e54cd800 ...

"Common-Cleaner" #11 ... tid=0x00000250e54cf000 ...

"Thread-0" #12 ... tid=0x00000250e54d1800 ...

"Thread-1" #13 ... tid=0x00000250e54d2000 ...

"DestroyJavaVM" #14 ... tid=0x00000250e54d0800 ..

Threads

直接跟踪SMR信息是线程列表。我们的死锁程序中列出的第一个线程是 Reference Handler 线程:

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x00000250e4979000 nid=0x3c28 waiting on condition [0x000000b82a9ff000]

java.lang.Thread.State: RUNNABLE

at java.lang.ref.Reference.waitForReferencePendingList(java.base@10.0.1/Native Method)

at java.lang.ref.Reference.processPendingReferences(java.base@10.0.1/Reference.java:174)

at java.lang.ref.Reference.access$000(java.base@10.0.1/Reference.java:44)

at java.lang.ref.Reference$ReferenceHandler.run(java.base@10.0.1/Reference.java:138)

Locked ownable synchronizers:

- None

主题摘要

每个线程的第一行表示线程摘要,其中包含以下项目:

部分

描述

名称

"Reference Handler"

线程的可读的名称。这个名称可以通过调用对象setName上的方法来设置,并通过调用Thread对象来获得getName。

ID

#2

与每个Thread 对象关联的唯一ID 。系统中的所有线程从1开始生成该号码 。每次 Thread 创建对象时,顺序号都会递增,然后分配给新创建的对象Thread。这个ID是只读的,可以通过调用getId 一个 Thread 对象来获得 。

守护进程状态

daemon

表示线程是否为守护进程线程的标记。如果线程是守护进程,则该标记将存在; 如果线程是非守护线程,则不会出现标记。例如, Thread-0 不是守护进程线程,因此daemon 在其摘要中没有标记:Thread-0" #12 prio=5...。

优先

prio=10

Java线程的数字优先级。请注意,这不一定对应于调度Java线程时操作系统线程的优先级。Thread 对象的优先级 可以使用该setPriority 方法来设置并使用该 方法来获得 getPriority 。

OS线程优先级

os_prio=2

操作系统线程优先。此优先级可以与Java线程优先级不同,并且对应于调度Java线程的OS线程。

地址

tid=0x00000250e4979000

Java线程的地址。该地址表示Java本地接口(JNI)本机 Thread 对象(Thread 通过JNI支持Java线程的C ++ 对象)的指针地址。通过将(指向Java Thread 对象的C ++对象的)指针转换为以下行的第879行 hotspot/share/runtime/thread.cpp的整数来获取此值 :

st->print("tid=" INTPTR_FORMAT " ", p2i(this));

尽管此项(tid)的关键字可能看起来是线程ID,但它实际上是底层JNI C ++ Thread 对象的地址, 因此不是调用getId Java Thread 对象时返回的ID 。

OS线程标识

nid=0x3c28

Java Thread 映射到的操作系统线程的唯一标识 。该值打印在以下行的第42行上hotspot/share/runtime/osThread.cpp:

st->print("nid=0x%x ", thread_id());

状态

waiting on condition

描述线程当前状态的可读字符串。该字符串提供了基本线程状态之外的补充信息(见下文),并且可以用于发现线程的预期操作(即线程试图获取锁或在阻塞时等待条件)。

最后一个已知的Java堆栈指针

[0x000000b82a9ff000]

与线程关联的堆栈的最后一个已知堆栈指针(SP)。该值使用本机C ++代码提供,并Thread 使用JNI 与Java 类交错 。该值是使用本last_Java_sp() 机方法获得的 ,并被格式化为hotspot / share / runtime / thread.cpp的第2886行的Thread Dump:

st->print_cr("[" INTPTR_FORMAT "]",

(intptr_t)last_Java_sp() & ~right_n_bits(12));

对于简单Thread Dump,此信息可能没有用处,但对于更复杂的诊断,此SP值可用于通过程序跟踪锁定获取。

线程状态

第二行代表线程的当前状态。在Thread.State 枚举中捕获线程的可能状态:

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED

有关每个州的含义的更多信息,请参阅 文档。 Thread.State

线程堆栈跟踪

下一部分包含转储时线程的堆栈跟踪。此堆栈跟踪类似于发生未捕获异常时所打印的堆栈跟踪,并且仅表示在执行转储时线程正在执行的类和行。在Reference Handler 线程的情况下 ,我们在堆栈跟踪中看到的并不是什么特别重要的事情,但是如果我们查看堆栈跟踪 Thread-02,我们会看到与标准堆栈跟踪有所不同:

"Thread-0" #12 prio=5 os_prio=0 tid=0x00000250e54d1800 nid=0xdec waiting for monitor entry [0x000000b82b4ff000]

java.lang.Thread.State: BLOCKED (on object monitor)

at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34)

- waiting to lock <0x00000000894465b0> (a java.lang.Object)

- locked <0x00000000894465a0> (a java.lang.Object)

at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)

Locked ownable synchronizers:

- None

在这个堆栈跟踪中,我们可以看到已经添加了锁定信息,这告诉我们这个线程正在等待一个地址为0x00000000894465b0 (类型 java.lang.Object)的对象的锁, 并且在堆栈跟踪的这一点上,地址0x00000000894465a0 锁定了(也是类型java.lang.Object)的对象。这个补充锁信息在诊断死锁时很重要,我们将在后面的章节中看到。

锁定的可持有同步器

线程信息的最后一部分包含一个同步器列表(可用于同步的对象,例如锁),它们完全由线程拥有。根据官方的Java文档,“一个可持有的同步器多半是线程独有并且使用了AbstractOwnableSynchronizer(或是其子类)去实现它的同步特性,ReentrantLock与ReentrantReadWriteLock就是JAVA平台提供的两个例子。有关锁定的拥有同步器的更多信息,请参阅此堆栈溢出文章。

JVM线程

Thread Dump的下一部分包含绑定到OS的JVM内部(非应用程序)线程。由于这些线程在Java应用程序中不存在,因此它们没有线程ID。这些线程通常由GC线程和JVM用于运行和维护Java应用程序的其他线程组成:

"VM Thread" os_prio=2 tid=0x00000250e496d800 nid=0x1920 runnable 
"GC Thread#0" os_prio=2 tid=0x00000250c35b5800 nid=0x310c runnable 
"GC Thread#1" os_prio=2 tid=0x00000250c35b8000 nid=0x12b4 runnable 
"GC Thread#2" os_prio=2 tid=0x00000250c35ba800 nid=0x43f8 runnable 
"GC Thread#3" os_prio=2 tid=0x00000250c35c0800 nid=0x20c0 runnable 
"G1 Main Marker" os_prio=2 tid=0x00000250c3633000 nid=0x4068 runnable 
"G1 Conc#0" os_prio=2 tid=0x00000250c3636000 nid=0x3e28 runnable 
"G1 Refine#0" os_prio=2 tid=0x00000250c367e000 nid=0x3c0c runnable 
"G1 Refine#1" os_prio=2 tid=0x00000250e47fb800 nid=0x3890 runnable 
"G1 Refine#2" os_prio=2 tid=0x00000250e47fc000 nid=0x32a8 runnable 
"G1 Refine#3" os_prio=2 tid=0x00000250e47fd800 nid=0x3d00 runnable 
"G1 Young RemSet Sampling" os_prio=2 tid=0x00000250e4800800 nid=0xef4 runnable 
"VM Periodic Task Thread" os_prio=2 tid=0x00000250e54d6800 nid=0x3468 waiting on condition

JNI全局引用

本部分捕获由JVM通过JNI维护的全局引用的数量。这些引用可能会在某些情况下导致内存泄漏,并且不会自动收集垃圾。

JNI global references: 2

对于许多简单的问题,这些信息是未使用的,但了解这些全球引用的重要性非常重要。有关更多信息,请参阅此堆栈溢出文章。

死锁的线程

Thread Dump的最后一节包含有关发现的死锁的信息。情况并非总是如此:如果应用程序没有检测到一个或多个死锁,则此部分将被忽略。由于我们的应用程序被设计为死锁,因此Thread Dump会使用以下消息正确捕获此争用:

Found one Java-level deadlock:

=============================

"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"

Java stack information for the threads listed above:

===================================================

"Thread-0":

at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34)

- waiting to lock <0x00000000894465b0> (a java.lang.Object)

- locked <0x00000000894465a0> (a java.lang.Object)

at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)

"Thread-1":

at DeadlockProgram$DeadlockRunnable.run(DeadlockProgram.java:34)

- waiting to lock <0x00000000894465a0> (a java.lang.Object)

- locked <0x00000000894465b0> (a java.lang.Object)

at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)

Found 1 deadlock.

第一小节描述了死锁情况: Thread-0 正在等待锁定监视器(在我们的应用程序中通过synchronized 声明的对象firstResource 和 secondResource ,这 Thread-1 是等待锁定被Thread-0保持的监视器。这个循环依赖是死锁的教科书定义(由我们的应用程序设计),如下图所示:

除了死锁的描述之外,所涉及的每个线程的堆栈跟踪都被打印在第二小节中。这使我们能够追踪导致死锁的行和锁(在这种情况下用作监视器锁的对象)。例如,如果我们检查我们的应用程序的第34行,我们会发现以下内容:

printLockedResource(secondResource);

这一行表示synchronized 导致死锁的块的第一行, 并提示我们在 secondResource 上的synchronize 是死锁的根源。为了解决这个僵局,我们就必须在resourceA 和resourceB中取代关键字synchronize resourceA ,并在两个线程中操持相同顺序。如果我们这样做,我们会得到以下应用程序:

public class DeadlockProgram {

public static void main(String[] args) throws Exception {

Object resourceA = new Object();

Object resourceB = new Object();

Thread threadLockingResourceAFirst = new Thread(new DeadlockRunnable(resourceA, resourceB));

Thread threadLockingResourceBFirst = new Thread(new DeadlockRunnable(resourceA, resourceB));//注意和上面的上线创建时对象顺序一致!

threadLockingResourceAFirst.start();

Thread.sleep(500);

threadLockingResourceBFirst.start();

}

private static class DeadlockRunnable implements Runnable {

private final Object firstResource;

private final Object secondResource;

public DeadlockRunnable(Object firstResource, Object secondResource) {

this.firstResource = firstResource;

this.secondResource = secondResource;

}

@Override

public void run() {

try {

synchronized (firstResource) {

printLockedResource(firstResource);

Thread.sleep(1000);

synchronized (secondResource) {

printLockedResource(secondResource);

}

}

} catch (InterruptedException e) {

System.out.println("Exception occurred: " + e);

}

}

private static void printLockedResource(Object resource) {

System.out.println(Thread.currentThread().getName() + ": locked resource -> " + resource);

}

}

}

此应用程序会生成以下输出并完成而不会发生死锁(请注意,Object 对象的地址 因执行而异):

Thread-0: locked resource -> java.lang.Object@1ad895d1

Thread-0: locked resource -> java.lang.Object@6e41d7dd

Thread-1: locked resource -> java.lang.Object@1ad895d1

Thread-1: locked resource -> java.lang.Object@6e41d7dd

总之,只使用Thread Dump中提供的信息,我们可以找到并修复死锁的应用程序。虽然这种检查技术对于许多简单的应用程序(或只有少量死锁的应用程序)已足够,但处理更复杂的Thread Dump可能需要以不同的方式处理。

处理更复杂的Thread Dump

处理生产应用程序时,Thread Dump可能会非常快速地变得非常困难。单个JVM可能有数百个线程同时运行,并且死锁可能涉及两个以上的线程(或者可能有多个并发问题作为单个原因的副作用),并且通过这些信息解析可能会乏味和不羁。

为了处理这些大规模的情况,Thread Dump分析器(TDAs)应该成为首选工具。这些工具解析JavaThread Dump,以易于管理的形式显示混淆信息(通常使用图形或其他视觉辅助工具),甚至可以对转储进行静态分析以发现问题。虽然情况的最佳工具会因情况而异,但一些最常见的TDA包括以下内容:

  • fastThread
  • Spotify TDA
  • IBM Thread and Monitor Dump Analyze for Java
  • irockel TDA

虽然这远远不是全面的TDA列表,但它们都执行足够的分析和视觉排序,以减少对Thread Dump进行解密的手动负担。

结论

Thread Dump是分析Java应用程序状态,尤其是错误操作,多线程应用程序状态的极好机制,但如果没有正确的知识,它们可能会快速给已经很困难的问题添加更多混淆。在本文中,我们开发了一个死锁应用程序并生成了卡住程序的Thread Dump。在分析转储后,我们找到了死锁的根本原因并相应地修复了它。这并不总是那么容易,对于许多生产应用来说,可能需要TDA的帮助。

无论哪种情况,每个专业Java开发人员都应该了解Thread Dump的基础知识,包括其结构,可以从中获取的信息以及如何利用它们来找出常见多线程问题的根本原因。虽然Thread Dump并非是所有多线程问题的灵丹妙药,但它是量化和降低诊断Java应用程序世界中的常见问题的复杂性的重要工具。

举报
评论 0