分析线程转储对于确定多线程进程中的问题非常有用,可以通过可视化单个线程转储的状态来解决死锁、锁争用和过多的CPU利用率等问题。
通过在分析线程转储后纠正每个线程的状态,可以实现应用程序的最大吞吐量。例如,假设一个进程占用了大量CPU,我们可以找出是否有哪个线程占用CPU最多。如果存在这样的线程,我们将其LWP数转换为十六进制数。然后,从线程转储中,我们可以找到nid等于先前获得的十六进制数的线程。使用线程的堆栈跟踪,我们可以查明问题所在。
例如通过下面命令找出线程的进程id
ps -mo pid,lwp,stime,time,cpu -C 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)
其整体主要包含四部分
- Thread Summary(线程摘要)
- Thread State(线程状态)
- Thread Stack Trace(线程堆栈跟踪)
- 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文件分析
分析的思路
- 如果要解决CPU消耗过高的问题,且已经知道是线程的ID,转换成十六进制后直接查找即可
- 如果是解决类似响应慢的问题,则可以从下面几步出发
- 间隔一两秒获取多份Thread Dump文件
- 将多份文件导入到工具中(IBM TMDA)对文件进行对比,找一直没结束的线程
- 首先,在90%的情况下,会有很多不相关的线程,关注应用程序工作所在的线程
- 分析这些线程的栈数据,找出一直没结束的原因,需要注意的关键有:
- 堆栈中有什么特别的吗?例如,您是否看到一个特定的应用程序堆栈框架总是位于顶部?或者您是否在中间的某个地方看到一个特定的应用程序堆栈帧,表明某个特定的函数很慢?
- 部分或大部分线程是否在后端服务(如数据库或 Web 服务)上等待?如果是这样,您能否从堆栈中找出这些是否来自特定的应用程序功能?
- 如果有死锁,那么你应该重点关注了
监视器分析对于发现Java锁瓶颈也很重要。单击Monitor Detail或Compare Monitors按钮来查看阻塞线程的层次结构。请记住,有些阻塞线程是正常的,例如线程池中等待下一项工作的线程。
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。
- 否则,发生以下顺序:
- 线程t被添加到对象m的等待集,并在m上执行n个解锁操作。
- 线程t不执行任何进一步的指令,直到它被从m的等待集中删除。该线程可能由于以下任何一个操作被从等待集中删除,并将在之后的某个时间恢复:
- 在m上执行的通知操作,其中t被选择从等待集中删除。
- 在m上执行的notifyAll操作。
- 在t上执行的中断操作。
- 如果这是一个定时等待,则从m的等待集中删除t的内部操作发生在至少毫秒毫秒加上毫秒毫秒后,自从这个等待操作开始。 实现的内部操作。虽然不鼓励,但允许实现执行“伪唤醒”,即从等待集中删除线程,从而在没有明确指示的情况下恢复线程。
注意,这一规定要求Java编码实践只在循环中使用wait,循环仅在线程等待的逻辑条件成立时终止。
- 每个线程必须确定可能导致它从等待集中删除的事件的顺序。该顺序不必与其他顺序一致,但线程必须表现得好像这些事件是按该顺序发生的。
例如,如果线程t在m的等待集中,然后t的中断和m的通知都发生,这些事件必须有一个顺序。如果认为中断先发生,那么t最终将抛出InterruptedException从等待中返回,并且m的等待集中的其他线程(如果在通知时存在的话)必须接收到通知。如果认为通知先发生,那么t最终将正常地从等待中返回,但中断仍悬而未决。 注:这里的“伪唤醒”是指在等待集中的线程被唤醒,而非线程本身。
- 线程t在m上执行n个锁操作。
- 如果线程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从等待中返回,那么等待集中的其他线程必须被通知。
四、参考文档
五、常用分析工具
- FastThread(在线工具)
- JStack(在线工具)
- JProfiler
- IBM TMDA
- Irockel TDA
- jvisualvm
- jcmd命令