1.如何获取线程 dump (java-core)文件
#1.jstack
jstack -l <pid> >> <file-path>
如: jstack -l 37320 > /opt/tmp/threadDump.txt
#2.kill -3
kill -3 <pid>
#3.JVisualVM图形工具采集
注意:
>> 在实际运行中,往往一次 dump的信息,还不足以确认问题。
建议多次 dump,寻找其中的共性与不同点。
分析工具参考: https://www.jianshu.com/p/200416bc3964
2.线程分析
2.1 线程状态分析
2.1.1 Runnable
该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。
一般情况下处于运行状态线程是会消耗CPU的,但不是所有的RUNNABLE都会消耗CPU,
比如线程进行网络IO时,这时线程状态是挂起的,但由于挂起发生在本地代码,虚拟机并不感知,
所以不会像显示调用Java的sleep()或者wait()等方法进入WAITING状态,只有等数据到来时才消耗一点CPU.
2.1.2 BLOCKED
此时的线程处于阻塞状态,一般是在等待进入一个临界区“waiting for monitor entry”,这种状态是需要重点关注的.
###Waiting for monitor entry
在多线程的 JAVA程序中,实现线程之间的同步,就要说说Monitor。
Monitor是 Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。
每一个对象都有,也仅有一个 monitor。
每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,
而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。
在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,
而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。
先看 “Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。
当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。对应的 code就像:
synchronized(obj) {
.........
}
这时有两种可能性:
>> 该 monitor 不被其它线程拥有, Entry Set里面也没有其它等待线程。
本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码. 此时为Runnable.
>> 该 monitor 被其它线程拥有,本线程在 Entry Set队列中等待。 此时为BLOCKED.
#BLOCKED状态下, 对应的堆栈示例:
"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8]
at testthread.WaitThread.run(WaitThread.java:39)
- waiting to lock <0xef63bf08> (a java.lang.Object)
- locked <0xef63beb8> (a java.util.ArrayList)
at java.lang.Thread.run(Thread.java:595)
临界区(synchronized)的设置,是为了保证其内部的代码执行的原子性和完整性。
但是因为临界区在任何时间只允许线程串行通过,这 和我们多线程的程序的初衷是相反的。
如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,
会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。
如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序。
2.1.3 TIMED_WAITING/WATING
表示线程被挂起,必须等待notify()或notifyAll()或unpark()或接收到interrupt信号才能退出等待状态.
>> 当设置超时时间时状态为TIMED_WAITING;
>> 如果是未设置超时时间,这时的状态为WATING.
#TIMED_WAITING/WATING下还需要关注下面几个线程状态:
###1.waiting on condition:
说明线程等待另一个条件的发生,来把自己唤醒, 该状态出现在线程等待某个条件的发生。
具体是什么原因,可以结合 stacktrace来分析。
最常见的情况是线程在等待网络的读写,比如当网络数据没有准备好读时,线程处于这种等待状态,
而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。
在 Java引入 NIO之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,
即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。
在 NIO里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。
如果发现有大量的线程都在处在 Wait on condition,从线程 stack看, 正等待网络读写,这可能是一个网络瓶颈的征兆。
因为网络阻塞导致线程无法执行。
一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;
另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。
所以要结合系统的一些性能观察工具来综合分析,
比如 netstat统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制;
另外一种出现 Wait on condition的常见情况是该线程在 sleep,等待 sleep的时间到了时候,将被唤醒。
###2.in Object.wait() / on object monitor:
说明该线程正在执行obj.wait()方法,放弃了 Monitor,进入 “Wait Set”队列. 那么线程为什么会进入 “Wait Set” 呢?
当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,
它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。
只有当别的线程在该对象上调用了 notify() 或者 notifyAll() , “ Wait Set”队列中线程才得到机会去竞争,
但是只有一个线程获得对象的 Monitor,恢复到运行态。
在 “Wait Set”中的线程, DUMP中表现为: in Object.wait(),类似于:
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000002d34800 nid=0x2b50 in Object.wait() [0x0000000018adf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
仔细观察上面的 DUMP信息,你会发现它有以下两行:
- locked <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
- waiting on <0x00000000d5f06b68> (a java.lang.ref.Reference$Lock)
这里需要解释一下,为什么先 lock了这个对象,然后又 waiting on同一个对象呢?让我们看看这个线程对应的代码:
synchronized(obj) {
.........
obj.wait();
.........
}
线程的执行中,先用 synchronized 获得了这个对象的 Monitor(对应于 locked <0x00000000d5f06b68> )。
当执行到 obj.wait(), 线程即放弃了 Monitor的所有权,进入 “wait set”队列(对应于 waiting on <0xef63beb8> )。
往往在你的程序中,会出现多个类似的线程,他们都有相似的 DUMP信息。这也可能是正常的。
比如,在程序中,有多个服务线程,设计成从一个队列里面读取请求数据。
这个队列就是 lock 以及 waiting on的对象。
当队列为空的时候,这些线程都会在这个队列上等待,直到队列有了数据,这些线程被 Notify,
当然只有一个线程获得了 lock,继续执行,而其它线程继续等待。
2.1.x 阻塞和等待的区别
#阻塞状态的线程:
是在等待一个排它锁,直到别的线程释放该排它锁,该线程获取到该锁才能退出阻塞状态;
堆栈信息显示为waiting for monitor entry, 在Entry Set中.
即代码尚未走进synchronized块.
synchronized(obj) {
...
}
#等待状态的线程:
则是等待一段时间,由系统唤醒或者别的线程唤醒,该线程便退出等待状态。
堆栈信息显示为 in object.wait() / on object monitor, 在Wait Set 中.
即代码走到了下文的 obj.wait();
synchronized(obj) {
...
obj.wait();
...
}
2.2 JDK 5.0 的 Lock
上面我们提到如果 synchronized和 monitor 机制运用不当,可能会造成多线程程序的性能问题。
在 JDK 5.0中,引入了 Lock机制,从而使开发者能更灵活的开发高性能的并发多线程程序,
可以替代以往 JDK中的 synchronized和 Monitor的 机制。
但是,要注意的是,因为 Lock类只是一个普通类, JVM无从得知 Lock对象的占用情况,
所以在线程 DUMP中,也不会包含关于 Lock的信息, 关于死锁等问题,就不如用 synchronized的编程方式容易识别。
https://www.jianshu.com/p/8a4a519e2f13 (可见此文 4.反例)
2.3 热锁
热锁,也往往是导致系统性能瓶颈的主要因素。其表现特征为,由于多个线程对临界区,或者锁的竞争,可能出现:
>> 频繁的线程的上下文切换:
从操作系统对线程的调度来看,当线程在等待资源而阻塞的时候,操作系统会将之切换出来,
放到等待的队列,当线程获得资源之后,调度算法会将这个线程切换进去,放到执行队列中。
>> 大量的系统调用:
因为线程的上下文切换,以及热锁的竞争,或者临界区的频繁的进出,都可能导致大量的系统调用。
>> 大部分 CPU开销用在 “系统态 ”:
线程上下文切换,和系统调用,都会导致 CPU在 “系统态 ”运行,换而言之,
虽然系统很忙碌,但是 CPU用在 “用户态 ”的比例较小,应用程序得不到充分的 CPU资源。
>> 随着 CPU数目的增多,系统的性能反而下降。因为 CPU数目多,同 时运行的线程就越多,
可能就会造成更频繁的线程上下文切换和系统态的 CPU开销,从而导致更糟糕的性能。
从整体的性能指标看,由于线程热锁的存在,程序的响应时间会变长,吞吐量会降低。
2.4 哪些线程状态占用CPU?
处于TIMED_WAITING、WATING、BLOCKED状态的线程是不消耗CPU的,
而处于 RUNNABLE 状态的线程要结合当前线程代码的性质判断是否消耗CPU:
>> 纯java运算代码,并且未被挂起,是消耗CPU的;
>> 网络IO操作,在等待数据时是不消耗CPU的;
参考资料
https://docs.oracle.com/cd/E15289_01/JRJDK/using_threaddumps.htm (Oracle官网)
https://www.iteye.com/blog/jameswxx-1041173
https://www.cnblogs.com/yuandengta/p/12900608.html (深入分析Object.notify/wait机制)
https://www.cnblogs.com/perfma/p/12515665.html