• 深入研究Java线程Dump分析:掌握发现和解决多线程问题的关键技巧


    1 Thread Dump介绍

    1.1 什么是Thread Dump

    Thread Dump是非常有用的诊断Java应用问题的工具。每一个Java虚拟机都有及时生成所有线程在某一点状态的thread-dump的能力,虽然各个 Java虚拟机打印的thread dump略有不同,但是大多都提供了当前活动线程的快照,及JVM中所有Java线程的堆栈跟踪信息,堆栈信息一般包含完整的类名及所执行的方法,如果可能的话还有源代码的行数。

    1.2 Thread Dump特点

    

    4de0bf39e27c701bb84d118cecff72d8.jpeg

    

    1.3 Thread Dump抓取

    一般当服务器挂起,崩溃或者性能低下时,就需要抓取服务器的线程堆栈(Thread Dump)用于后续的分析。在实际运行中,往往一次 dump的信息,还不足以确认问题。为了反映线程状态的动态变化,需要接连多次做thread dump,每次间隔10-20s,建议至少产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性。

    

    1.操作系统命令获取ThreadDump

    

    94bbff8388f0269b0f076a6219a5e39a.jpeg

    

    注意:

    

    e69f6df7805784155dfea5ba90005fa8.jpeg

    

    2.JVM 自带的工具获取线程堆栈

    

    9b213c3a8a102ff516ad45e5d7138039.jpeg

    

    2.1 Thread Dump信息

    1.头部信息:时间,JVM信息

    

    11ea844d7ef400606cd5cac89750c2b9.jpeg

    

    2.线程INFO信息块:

    

    72d1a562fddaa27d84fc2254bb994aa4.jpeg

    

    de00eeb08a0223e1e73ba1f912ec8831.jpeg

    

    3.Java thread statck trace详解:

    

    堆栈信息应该逆向解读:程序先执行的是第7行,然后是第6行,依次类推。

    

    142dcadd568408d2b92a76bc88cd598b.jpeg

    

    也就是说对象先上锁,锁住对象0xb3885f60,然后释放该对象锁,进入waiting状态。为啥会出现这样的情况呢?看看下面的java代码示例,就会明白:

    

    5b34ed1f40fa45d49ca35386b53bb3f2.jpeg

    

    408bdc737b86f36bc7a468deb9f600ee.jpeg

    

    在堆栈的第一行信息中,进一步标明了线程在代码级的状态,例如:

    

    ba531c6d6d9d06e83d63b9743c327864.jpeg

    

    解释如下

    

    e88457eae97736c2ae47e62bef2e1948.jpeg

    

    2.2 Thread状态分析

    线程的状态是一个很重要的东西,因此thread dump中会显示这些状态,通过对这些状态的分析,能够得出线程的运行状况,进而发现可能存在的问题。线程的状态在Thread.State这个枚举类型中定义

    

    cf0ee6402e26c426e9df550df5ad7b13.jpeg

    

    1.NEW

    

    每一个线程,在堆内存中都有一个对应的Thread对象。Thread t = new Thread();当刚刚在堆内存中创建Thread对象,还没有调用t.start()方法之前,线程就处在NEW状态。在这个状态上,线程与普通的java对象没有什么区别,就仅仅是一个堆内存中的对象

    

    2.RUNNABLE

    

    该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。 这个状态的线程比较正常,但如果线程长时间停留在在这个状态就不正常了,这说明线程运行的时间很长(存在性能问题),或者是线程一直得不得执行的机会(存在线程饥饿的问题)。

    

    3.BLOCKED

    

    线程正在等待获取java对象的监视器(也叫内置锁),即线程正在等待进入由synchronized保护的方法或者代码块。synchronized用来保证原子性,任意时刻最多只能由一个线程进入该临界区域,其他线程只能排队等待。

    

    4.WAITING

    

    处在该线程的状态,正在等待某个事件的发生,只有特定的条件满足,才能获得执行机会。而产生这个特定的事件,通常都是另一个线程。也就是说,如果不发生特定的事件,那么处在该状态的线程一直等待,不能获取执行的机会。比如:

    

    971c2d42eeeb09549a4427abd971c44b.jpeg

    

    5.TIMED_WAITING

    

    J.U.C中很多与线程相关类,都提供了限时版本和不限时版本的API。TIMED_WAITING意味着线程调用了限时版本的API,正在等待时间流逝。当等待时间过去后,线程一样可以恢复运行。如果线程进入了WAITING状态,一定要特定的事件发生才能恢复运行;而处在TIMED_WAITING的线程,如果特定的事件发生或者是时间流逝完毕,都会恢复运行

    

    6.TERMINATED

    

    线程执行完毕,执行完run方法正常返回,或者抛出了运行时异常而结束,线程都会停留在这个状态。这个时候线程只剩下Thread对象了,没有什么用了。

    

    2.3 关键状态分析

    1.Wait on conditionThe thread is either sleeping or waiting to be notified by another thread.

    

    该状态说明它在等待另一个条件的发生,来把自己唤醒,或者干脆它是调用了 sleep(n)。

    

    此时线程状态大致为以下几种:

    

    4d10da6b86ff592dc57331815678f7f8.jpeg

    

    2.Waiting for Monitor Entry 和 in Object.wait()The thread is waiting to get the lock for an object (some other thread may be holding the lock). This happens if two or more threads try to execute synchronized code. Note that the lock is always for an object and not for individual methods.

    

    在多线程的JAVA程序中,实现线程之间的同步,就要说说 Monitor。Monitor是Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者Class的锁。每一个对象都有,也仅有一个 Monitor。下面这个图,描述了线程和 Monitor之间关系,以及线程的状态转换图:

    

    5d2a4d94753ce5881166cb3a5f14d58e.jpeg

    

    如上图,每个Monitor在某个时刻,只能被一个线程拥有,该线程就是 “ActiveThread”,而其它线程都是 “Waiting Thread”,分别在两个队列“Entry Set”和“Wait Set”里等候。在“Entry Set”中等待的线程状态是“Waiting for monitor entry”,而在“Wait Set”中等待的线程状态是“in Object.wait()”。

    

    先看“Entry Set”里面的线程。我们称为 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了“Entry Set”队列。对应的 code就像:

    

    65dacc6bfafd13295c6459ef72726b6a.jpeg

    

    这时有两种可能性:

    

    377b5d8ea36615aca47dadc22704023a.jpeg

    

    在第一种情况下,线程将处于 “Runnable”的状态,而第二种情况下,线程 DUMP会显示处于 “waiting for monitor entry”。如下:

    

    fcd6b1709ad7939cc05931814d1f96ac.jpeg

    

    临界区的设置,是为了保证其内部的代码执行的原子性和完整性。但是因为临界区在任何时间只允许线程串行通过,这和我们多线程的程序的初衷是相反的。如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序。

    

    再看“Wait Set”里面的线程。当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃 Monitor,进入 “Wait Set”队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll(),“Wait Set”队列中线程才得到机会去竞争,但是只有一个线程获得对象的Monitor,恢复到运行态。在 “Wait Set”中的线程, DUMP中表现为: in Object.wait()。如下:

    

    9bda80334940ac4297de446848fb0af4.jpeg

    

    综上,一般CPU很忙时,则关注runnable的线程,CPU很闲时,则关注waiting for monitor entry的线程。

    

    3.JDK 5.0 的 Lock

    

    上面提到如果 synchronized和 monitor机制运用不当,可能会造成多线程程序的性能问题。在 JDK 5.0中,引入了 Lock机制,从而使开发者能更灵活的开发高性能的并发多线程程序,可以替代以往 JDK中的 synchronized和 Monitor的 机制。但是,要注意的是,因为 Lock类只是一个普通类,JVM无从得知 Lock对象的占用情况,所以在线程 DUMP中,也不会包含关于 Lock的信息, 关于死锁等问题,就不如用 synchronized的编程方式容易识别。

    

    2.4 关键状态示例

    显示BLOCKED状态

    

    4050c467fd5bea67a72d51ff94778fb6.jpeg

    

    先获取object的线程会执行5分钟,这5分钟内会一直持有object的监视器,另一个线程无法执行处在BLOCKED状态

    

    7192c93ccf7a60fb12050b4761fca2c8.jpeg

    

    通过thread dump可以看到:t2线程确实处在BLOCKED (on object monitor)。waiting for monitor entry 等待进入synchronized保护的区域

    

    2.显示WAITING状态

    

    5e40a7820f2516856c8b77639d204ff0.jpeg

    

    6eb20aed446f47539969535ff3ac589f.jpeg

    

    可以发现t1和t2都处在WAITING (on object monitor),进入等待状态的原因是调用了in Object.wait()。通过J.U.C包下的锁和条件队列,也是这个效果,大家可以自己实践下。

    

    3.显示TIMED_WAITING状态

    

    24ce58657f02e652abf432df2ed7c826.jpeg

    

    95ca0e3ed4df41951a44e37740aaff32.jpeg

    

    可以看到t1和t2线程都处在java.lang.Thread.State: TIMED_WAITING (parking),这个parking代表是调用的JUC下的工具类,而不是java默认的监视器

    3.1 问题场景

    1.CPU飙高,load高,响应很慢

    

    a089b0231deba609f6b6be6ad843f61c.jpeg

    

    2.查找占用CPU最多的线程

    

    e3e7d676194987bfb4dcc9da86c3ac78.jpeg

    

    3.CPU使用率不高但是响应很慢

    

    be56c9579822a8d90f4ea1c5f553b6e4.jpeg

    

    4.请求无法响应

    

    e35140e46f6a1b1a394aed25e35375ce.jpeg

    

    3.2死锁

    死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从top或prstat的输出中消失。

    

    比如在下面这个示例中,是个较为典型的死锁情况:

    

    3712a4804bc7600f86156351970657ed.jpeg

    

    在 JAVA 5中加强了对死锁的检测。线程 Dump中可以直接报告出 Java级别的死锁,如下所示:

    

    478483b5ed350558216755a675758b6d.jpeg

    

    3.33热锁

    热锁,也往往是导致系统性能瓶颈的主要因素。其表现特征为:由于多个线程对临界区,或者锁的竞争,可能出现:

    

    7f4aeb80d1d3d2d23bddf9880b15cb6e.jpeg

    

    上面的描述,都是一个 scalability(可扩展性)很差的系统的表现。从整体的性能指标看,由于线程热锁的存在,程序的响应时间会变长,吞吐量会降低。

    

    那么,怎么去了解 “热锁 ”出现在什么地方呢

    

    4f0c9cdf2a6a99ff03fdd52e58d63dff.jpeg

    

    4. JVM重要线程

    JVM运行过程中产生的一些比较重要的线程罗列如下:

    

    07897330f26939615a26443fd2e174d6.jpeg
  • 相关阅读:
    [Gitlab CI/CD] 自动发布新版本
    使用 Python 的基于边缘和基于区域的分割
    JPA与sboot整合
    SQLAlchemy常用数据类型
    API网关与社保模块
    【Elastic】ElasticView 一款用来监控elasticsearch web可视化工具
    基于51单片机的双机串口通信proteus仿真原理图PCB
    全新 UI 震撼来袭!ng-matero v18 正式发布!
    MySQL 索引
    NLP模型常见报错(定期更新)
  • 原文地址:https://blog.csdn.net/m0_70952941/article/details/133925983