Java性能分析之线程栈详解(下)

Java性能分析之线程栈详解(下)

转载自:微信公众号“测试那点事儿

结合jstack结果对线程状态详解

上篇文章详细介绍了线程栈的作用、状态、任何查看理解,本篇文章结合jstack工具来查看线程状态,并列出重点关注目标。Jstack是常用的排查工具,它能输出在某一个时间,Java进程中所有线程的状态,很多时候这些状态信息能给我们的排查工作带来有用的线索。 Jstack的输出中,Java线程状态主要是以下几种:

1、BLOCKED 线程在等待monitor锁(synchronized关键字)

2、TIMED_WAITING 线程在等待唤醒,但设置了时限

3、WAITING 线程在无限等待唤醒

4、RUNNABLE 线程运行中或I/O等待

下面通过详细的实例来对这几种状态进行解释

BLOCKED

如下图所示,为使用jstack工具dump线程后,查看到的线程处于blocked状态。dump线程后,最先看的是线程所处的状态。这个线程处于Blocked状态,我们需要重点分析。

首先,我们来逐条分析下jstack工具抓取到的线程信息:

jstack工具抓取到的线程信息,是从下往上分析的,由上图可见,线程先是开始运行,之后运行业务的一些方法,直到调用 org.apache.log4j.Category.forcedLog之后,开始waiting to lock。

线程的状态是:BLOCKED (on object monitor)

说明线程处于阻塞状态,正在等待一个monitor lock。阻塞原因是:因为本线程与其他线程公用了一个锁,这时,已经有其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法。当本线程想要进入这个同步代码块时,也需要这个锁,但锁已被占用,从而导致本线程处于阻塞状态。

第一行中包含了线程名和id等信息,如上图中的"druid-consumer-pool-3",nid(每个线程都有线程pid,将该pid转成16进制的值,即为jstack结果中的nid,可以通过nid唯一确认一个线程。)

第一行中还有线程目前正在  waiting for monitor entry,还是表明了线程在等待进入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()”。目前线程状态为:waiting for monitor entry,说明它是“Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。

这时有两种可能性:

1、该 monitor不被其它线程拥有, Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码

2、该 monitor被其它线程拥有,本线程在 Entry Set队列中等待。 

在第一种情况下,线程将处于 “Runnable”的状态

而第二种情况下,线程 DUMP会显示处于 “waiting for monitor entry”

根据以上分析,我们可以看出,线程想要调用log4j,目的是打印日志,但是由于调用log4j写日志有锁机制,于是线程被阻塞了。再排查项目使用的log4j版本,得知此版本存在性能bug,优化手段为升级log4j版本或者调整日志级别、优化日志打印的内容,或者添加缓存。

 waiting to lock <地址>

说明线程使用synchronized申请对象锁未成功,于是开始等待别的线程释放锁。线程在监视器的进入区等待。这条一般在调用栈顶出现,线程状态一般对应为Blocked。

TIMED_WAITING

如下图所示,为使用jstack工具dump线程后,查看到的线程处于TIMED_WAITING状态。

线程的状态是:TIMED_WAITING

这时的线程处于sleep状态,说明线程在有时限的等待另一个线程的特定操作,一般会有超时时间唤醒。就一般情况来说,出现TIMED_WAITING很正常,等待网络IO等都会出现这种状态,但是大量的线程处于TIMED_WAITING时,需要我们重点分析。

第一行中,显示线程在waiting on condition,这说明线程在等待某个条件的发生,从而自己唤醒,或者是调用了 sleep(n)。

当线程在waiting on condition时,线程状态可能为:

1、java.lang.Thread.State: WAITING (parking):一直等某个条件发生;

2、java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定时等待某个条件发生,即使这个条件不到来,也将定时唤醒自己。

在我们这个例子里,线程处于 TIMED_WAITING状态。

parking to wait for <地址>目标

这里即为第一行“waiting on condition" 所等待的条件,等待是java.util.concurrent.CountDownLatch$Sync,这是一种闭锁的实现,是一种同步工具类,可以延迟线程的进度直到闭锁到达终止状态,其内部包含一个计数器,该计数器被初始化为一个整数,表示需要等待事件的数量。由以上分析可以知道,线程是因为向druid写数据,由于有同步机制,而进入TIMED_WAITING状态。

和上个例子线程在parking to wait for 不同,在这个例子中,线程也是处于TIMED_WAITING状态,但是第一行中显示线程正在 in Object.wait(),第四行显示线程waiting on <地址> 目标。

线程在in Object.wait(), 说明线程在获得了监视器之后,又调用了 java.lang.Object.wait() 方法。

上篇线程详解(一)中说过等待monitor 的线程分为两种

在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”

在 “Wait Set”中等待的线程状态是 “in Object.wait()”

本例是在“Wait Set”中等待的线程,其状态是in Object.wait(),这说明线程获得了 Monitor,但是线程继续运行的条件没有满足,则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。

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

1、java.lang.Thread.State: TIMED_WAITING (on object monitor);

2、java.lang.Thread.State: WAITING (on object monitor);

本例中线程就处于TIMED_WAITING状态。

WAITING

如下图所示,为使用jstack工具dump线程后,查看到的线程处于WAITING状态。

(1)线程的状态是:WAITING

意思就是线程在等待另外一个线程去解除它的等待状态。一个典型的例子就是生产者消费者模型,当生产者生产太慢的时候,消费者要等待生产者生产才能去消费,这段时间消费者线程就处于waiting状态。还可以使用lock.wait()方法使线程进入waiting状态,无超时的等待,必须等待lock.notify()或lock.notifyAll()或接收到interrupt信号才能退出等待状态。

(2)parking to wait for <地址> 目标

第一行中,显示线程在waiting on condition,这说明线程在等待某个条件的发生,从而自己唤醒。

当线程在waiting on condition时,线程状态可能为

java.lang.Thread.State: WAITING (parking):一直等某个条件发生;

java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定时等待某个条件发生,即使这个条件不到来,也将定时唤醒自己。

在这个例子里,线程处于 WAITING状态,parking to wait for所等待的是java.util.concurrent.locks.AbstractQueuedSynchronizer,这也是java实现同步机制。

RUNNABLE

如下图所示,为使用jstack工具dump线程后,查看到的线程处于RUNNABLE 状态。

 在这个例子里,可以清楚看到整个线程运行的过程。在线程运行过程中,有很多次获取锁,即为上图中locked <地址> 目标,即此线程使用synchronized申请对象锁成功,是监视器的拥有者,可以在临界区内进行操作。上图所lock的内容有java IO的输入输出流等。

"02'

在一次测试过程中,通过线程打印有了一个意外收获

如下面信息,“http-bio-18272-exec-258”,表示Tomcat 的启动模式为 bio模式,将bio模式改为nio模式,在该项目中,其他条件不变,只将bio模式更改为nio模式,tps提升了一倍

 tomcat的运行模式有3种.修改他们的运行模式.3种模式的运行是否成功,可以看他的启动控制台,或者启动日志.或者登录他们的默认页面http://localhost:8080/查看其中的服务器状态。 

1)bio :默认的模式,性能非常低下,没有经过任何优化处理和支持. 

2)nio :利用java的异步io护理技术,no blocking IO技术. 

想运行在该模式下,直接修改server.xml里的Connector节点,修改protocol为 

<Connector port="80" protocol="org.apache.coyote.http11.Http11NioProtocol"connectionTimeout="20000" URIEncoding="UTF-8" useBodyEncodingForURI="true"enableLookups="false" redirectPort="8443" />

启动后,就可以生效。 

3)apr 

安装起来最困难,但是从操作系统级别来解决异步的IO问题,大幅度的提高性能. 

必须要安装apr和native,直接启动就支持apr。

想获取更多测试技能,欢迎加入BestTest5000人交流群:435092293

原文地址:https://www.cnblogs.com/lyftest/p/8194868.html