分析线程转储对于确定多线程进程中的问题非常有用,可以通过可视化单个线程转储的状态来解决死锁、锁争用和过多的CPU利用率等问题。

通过在分析线程转储后纠正每个线程的状态,可以实现应用程序的最大吞吐量。例如,假设一个进程占用了大量CPU,我们可以找出是否有哪个线程占用CPU最多。如果存在这样的线程,我们将其LWP数转换为十六进制数。然后,从线程转储中,我们可以找到nid等于先前获得的十六进制数的线程。使用线程的堆栈跟踪,我们可以查明问题所在。

例如通过下面命令找出线程的进程id

ps -mo pid,lwp,stime,time,cpu -C java

Java Thread Dump文件分析_java

然后通过以下命令获取dump文件

jstack -l 26680 > javacore.txt



一、Thread Dump文件格式

"pool-22-thread-1" #601 prio=5 os_prio=0 tid=0x00007fac08154800 nid=0x606f waiting on condition [0x00007fab8033d000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000005ff9a78a0> (a 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.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
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)

Locked ownable synchronizers:
- <0x000000076b9b36c0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

其整体主要包含四部分

  1. Thread Summary(线程摘要)
  2. Thread State(线程状态)
  3. Thread Stack Trace(线程堆栈跟踪)
  4. Locked Ownable Synchronizer(锁定的可拥有同步器


整体格式说明

SECTION

DESCRIPTION

EXAMPLE

线程名称

线程的可读名称

"pool-22-thread-1"

线程编号

线程唯一ID

#601

守护进程状态

如果是非守护线程,则无该标记

daemon

线程优先级

Java 线程的优先级

prio=5

操作系统线程优先级

操作系统线程优先级

os_prio=0


Java 线程地址

该地址表示JNI原生Thread 对象的指针地址

tid=0x00007fac08154800

操作系统线程ID

Java 线程映射到的操作系统线程的唯一ID,和top命令查看的pid对应(不过是10进制的)

nid=0x606f

线程状态补充信息


线程状态之外的补充信息

waiting on condition

最后一个已知堆栈指针


该值是使用本机 C++ 代码提供的

[0x00007fab8033d000]

线程状态

线程的当前状态

java.lang.Thread.State: WAITING (parking)

线程调用栈追钟信息

此堆栈跟踪类似于发生未捕获的异常时打印的堆栈跟踪,并且仅表示进行转储时线程正在执行的类和行。

at sun.misc.Unsafe.park(Native Method)... ...

同步器列表

由线程独占的同步器(可用于同步的对象,如锁)的列表。根据官方的Java文档,“可拥有的同步器是一个线程专有的同步器,它使用AbstractOwnableSynchronizer(或其子类)来实现其同步属性。ReentrantLock和ReentrantReadWriteLock的写锁(而不是读锁)是平台提供的可拥有同步器的两个

Locked ownable synchronizers:- <0x000000076b9b36c0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)



线程状态

状态

说明

NEW

线程被创建但尚未启动

RUNNABLE

正在执行

BLOCKED

等待监视器锁定而被阻塞

WAITING

无限期等待另一个线程执行特定操作

TIMED_WAITING

等待另一个线程执行操作

TERMINATED

已退出的线程


线程状态补充信息

状态

说明

runnable

线程处于执行中

deadlock

死锁(重点关注)

blocked

线程被阻塞 (重点关注)

parked

停止

locked

对象加锁

waiting

线程正在等待

waiting to lock

等待上锁

Object.wait()

对象等待中

waiting for monitor entry

等待获取监视器(重点关注)

waiting on condition

等待资源(重点关注)



二、Thread Dump文件分析

分析的思路

  1. 如果要解决CPU消耗过高的问题,且已经知道是线程的ID,转换成十六进制后直接查找即可
  2. 如果是解决类似响应慢的问题,则可以从下面几步出发
  1. 间隔一两秒获取多份Thread Dump文件
  2. 将多份文件导入到工具中(IBM TMDA)对文件进行对比,找一直没结束的线程
  3. 首先,在90%的情况下,会有很多不相关的线程,关注应用程序工作所在的线程
  4. 分析这些线程的栈数据,找出一直没结束的原因,需要注意的关键有:
  • 堆栈中有什么特别的吗?例如,您是否看到一个特定的应用程序堆栈框架总是位于顶部?或者您是否在中间的某个地方看到一个特定的应用程序堆栈帧,表明某个特定的函数很慢?
  • 部分或大部分线程是否在后端服务(如数据库或 Web 服务)上等待?如果是这样,您能否从堆栈中找出这些是否来自特定的应用程序功能?
  1. 如果有死锁,那么你应该重点关注了

监视器分析对于发现Java锁瓶颈也很重要。单击Monitor DetailCompare Monitors按钮来查看阻塞线程的层次结构。请记住,有些阻塞线程是正常的,例如线程池中等待下一项工作的线程。

Java Thread Dump文件分析_堆栈_02    Java Thread Dump文件分析_堆栈_03


Java Thread Dump文件分析_Java_04    Java Thread Dump文件分析_Java_05


TMDA的使用参考:tools-thread-monitor-dump-analyzer-java-tmda


三、线程与锁


1、同步

Java语言中使用基于监视器(monitors)实现的同步(synchronization)来提供了多个线程间通信的机制。Java中的每个对象都与一个监视器(monitor)相关联,一个线程可以锁定(lock)或解锁(unlock)该监视器。同时,每次只能有一个线程可以持有一个监视器的锁,任何其他视图锁定该监视器的线程都被阻塞,直到他们可以获得该监视器得锁。

一个线程可以多次锁定一个特定的监视器,每次解锁都会逆转一次锁定操作。

一个synchronized方法在被调用时会自动执行一次锁操作,直到锁操作成功完成,它的主体才会被执行。

  • 如果该方法是一个实例方法,它锁定与被调用实例相关的监视器。
  • 如果该方法是一个静态方法,它锁定与表示方法定义的类的Class对象相关的监视器。

如果方法的主体已经执行完成,无论是否为正常执行完,一个解锁操作将自动在同一个监视器上执行。

除了以上的同步机制外, 如volatile变量的读写和java.util.concurrent包中的类的使用,也提供了可选的同步方式。


2、等待与唤醒

2.1、wait

每个对象,除了有一个关联的监视器,还有一个关联的等待集,该等待集是一组线程。 

等待集仅通过Object.wait,Object.notify和Object.notifyAll方法操作,将线程添加到等待集和从等待集中删除线程的基本操作是原子的。

假设thread t是线程,它在对象m上执行wait方法,假设n是t在m上未被解锁操作匹配的锁操作的数量。发生下列操作之一:

  • 如果n为零(即线程t还没有拥有目标m的锁),则抛出IllegalMonitorStateException。
  • 如果这是一个定时等待,并且毫秒参数不在0-999999范围内,或者毫秒参数为负数,则抛出IllegalArgumentException。
  • 如果线程t被中断,则抛出InterruptedException,并且t的中断状态设置为false。
  • 否则,发生以下顺序:
  1. 线程t被添加到对象m的等待集,并在m上执行n个解锁操作。
  2. 线程t不执行任何进一步的指令,直到它被从m的等待集中删除。该线程可能由于以下任何一个操作被从等待集中删除,并将在之后的某个时间恢复:
  • 在m上执行的通知操作,其中t被选择从等待集中删除。
  • 在m上执行的notifyAll操作。
  • 在t上执行的中断操作。
  • 如果这是一个定时等待,则从m的等待集中删除t的内部操作发生在至少毫秒毫秒加上毫秒毫秒后,自从这个等待操作开始。 实现的内部操作。虽然不鼓励,但允许实现执行“伪唤醒”,即从等待集中删除线程,从而在没有明确指示的情况下恢复线程。

注意,这一规定要求Java编码实践只在循环中使用wait,循环仅在线程等待的逻辑条件成立时终止。

  • 每个线程必须确定可能导致它从等待集中删除的事件的顺序。该顺序不必与其他顺序一致,但线程必须表现得好像这些事件是按该顺序发生的。
    例如,如果线程t在m的等待集中,然后t的中断和m的通知都发生,这些事件必须有一个顺序。如果认为中断先发生,那么t最终将抛出InterruptedException从等待中返回,并且m的等待集中的其他线程(如果在通知时存在的话)必须接收到通知。如果认为通知先发生,那么t最终将正常地从等待中返回,但中断仍悬而未决。 注:这里的“伪唤醒”是指在等待集中的线程被唤醒,而非线程本身。
  1. 线程t在m上执行n个锁操作。
  2. 如果线程t在步骤2中由于中断而从m的等待集中移除,那么t的中断状态被设置为false,等待方法抛出InterruptedException。


2.2 Notification

通知操作发生在调用notify和notifyAll方法时。

假设线程t是在对象m上执行这些方法中的任何一个的线程,假设n是t在m上尚未被解锁操作匹配的锁操作的数量。发生以下操作之一:

  • 如果n为零,则抛出IllegalMonitorStateException。 这是线程t还没有拥有目标m的锁的情况下。
  • 如果n大于零,并且这是一个通知操作,那么如果m的等待集不是空的,则选择m当前等待集的一个成员线程u并将其从等待集中移除。 没有保证等待集中哪个线程被选择。从等待集中移除使u在等待操作中恢复。然而,请注意,恢复后的u的锁操作不能成功,直到t完全解锁m的监视器一段时间后。
  • 如果n大于0,并且这是一个notifyAll操作,那么所有的线程都从m的等待集合中删除,并因此恢复。 注意,然而,在恢复等待期间,每次只有一个线程会锁定所需的监视器。


2.3 Interruptions

中断操作发生在调用Thread.interrupt时,以及定义的依次调用它的方法,如ThreadGroup.interrupt。

假设t是调用u.interrupt的线程,对于某个线程u,其中t和u可能是相同的。这个操作导致u的中断状态被设置为true。

此外,如果存在某个对象m,其等待集合包含u,那么u将从m的等待集合中删除。这使u能够在等待操作中恢复,在这种情况下,这个等待将在重新锁定m的监视器后抛出InterruptedException。

调用Thread.isInterrupted可以确定线程的中断状态。静态方法Thread.interrupted可以被线程调用以观察和清除自己的中断状态。


2.4 Interactions of Waits, Notification, and Interruption

上述规范允许我们确定几个属性,这些属性与等待、通知和中断的交互有关。

如果一个线程在等待时既被通知又被中断,它可能:

  • 被中断正常地从wait返回,同时仍然有一个挂起的中断(换句话说,调用Thread.interrupted将返回true)
  • 通过抛出InterruptedException从wait返回

线程可能不会重置其中断状态,并从调用wait正常返回。

同样,通知不会因为中断而丢失。假设一组线程s位于对象m的等待集中,另一个线程对m执行了一个通知。 那么:

  • 至少s中的一个线程必须正常地从等待中返回,或者
  • s中的所有线程必须抛出InterruptedException退出等待

注意,如果一个线程既被中断又被notify唤醒,并且该线程通过抛出InterruptedException从等待中返回,那么等待集中的其他线程必须被通知。



四、参考文档

  1. how-to-read-a-thread-dump
  2. Chapter 17. Threads and Locks


五、常用分析工具

  1. FastThread(在线工具)
  2. JStack(在线工具)
  3. JProfiler
  4. IBM TMDA
  5. Irockel TDA
  6. jvisualvm
  7. jcmd命令