JDK本身提供了很多方便的JVM性能调优监控工具,除了集成式的VisualVM和jConsole外,还有jps、jstack、jmap、jhat、jstat等小巧的工具,另外还有一些商业性的工具,如JProfiler等,相对于这些集成性的工具,在资源消耗方面会比较大,有时候一些线上临时性的需要抓取线程信息和堆信息的情况,这些集成性的工具使用起来就显得不“那么强大和好用了”,这个时候,反而一些简单的命令,可以帮我们解决燃眉之急,也非常简单好用,这次就先简单介绍一下线程相关的知识,以及常用工具的用法,帮助我们快速定位问题。
1、NEW
线程刚刚被创建,也就是已经new过了,但是还没有调用start()方法
2、RUNNABLE
RUNNABLE这个名字很具有欺骗性,很容易让人误以为处于这个状态的线程正在运行。事实上,这个状态只是表示,线程是可运行的。一个单核CPU在同一时刻,只能运行一个线程。
3、BLOCKED
线程处于阻塞状态,正在等待一个monitor lock。通常情况下,是因为本线程与其他线程公用了一个锁。其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法,而本线程进入这个同步代码块也需要这个锁,最终导致本线程处于阻塞状态。
4、WAITING
等待状态,调用以下方法可能会导致一个线程处于等待状态:
Object.wait
with no timeout
Thread.join
with no timeout
LockSupport.park
例如:对于wait()方法,一个线程处于等待状态,通常是在等待其他线程完成某个操作。本线程调用某个对象的wait()方法,其他线程处于完成之后,调用同一个对象的notify或者notifyAll()方法。Object.wait()方法只能够在同步代码块中调用。调用了wait()方法后,会释放锁。
5、TIMED_WAITING
线程等待指定的时间,对于以下方法的调用,可能会导致线程处于这个状态:
Th
read.sleep
Object.wait
with timeout
Thread.join
with timeout
LockSupport.parkNanos
LockSupport.parkUntil
6、TERMINATED
线程终止。
这些状态中NEW状态是开始,TERMINATED时销毁,在整个线程对象的运行过程中,这个两个状态只能出现一次。其他任何状态都可以出现多次,彼此之间可以相互转换。
以下是状态转化图,可以较为清晰地看到状态转换的场景与条件:
/借鉴部分开始---这部分直接借鉴了 赵小桐 同学的辛苦工作了,这部分内容写的相当详细,100个赞(ks地址:http://ks.netease.com/blog?id=7659)
二. 结合jstack结果对线程状态详解
下面结合jstack工具来查看线程状态,并列出重点关注目标。Jstack是常用的排查工具,它能输出在某一个时间,Java进程中所有线程的状态,很多时候这些状态信息能给我们的排查工作带来有用的线索。 Jstack的输出中,Java线程状态主要是以下几种:
下面通过详细的实例来对这几种状态进行解释:
1.BLOCKED
如下图所示,为使用jstack工具dump线程后,查看到的线程处于blocked状态。dump线程后,最先看的是线程所处的状态。这个线程处于Blocked状态,我们需要重点分析。
首先,我们来逐条分析下jstack工具抓取到的线程信息:
jstack工具抓取到的线程信息,是从下往上分析的,由上图可见,线程先是开始运行,之后运行业务的一些方法,直到调用 org.apache.log4j.Category.forcedLog之后,开始waiting to lock。
(1)线程的状态是:BLOCKED (on object monitor)
说明线程处于阻塞状态,正在等待一个monitor lock。阻塞原因是:因为本线程与其他线程公用了一个锁,这时,已经有其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法。当本线程想要进入这个同步代码块时,也需要这个锁,但锁已被占用,从而导致本线程处于阻塞状态。
(2)第一行中包含了线程名和id等信息,如上图中的"druid-consumer-pool-3",nid(每个线程都有线程pid,将该pid转成16进制的值,即为jstack结果中的nid,可以通过nid唯一确认一个线程。)
(3)第一行中还有线程目前正在 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()”。 |
根据以上分析,我们可以看出,线程想要调用log4j,目的是打印日志,但是由于调用log4j写日志有锁机制,于是线程被阻塞了。再排查项目使用的log4j版本,得知此版本存在性能bug,优化手段为升级log4j版本或者调整日志级别、优化日志打印的内容,或者添加缓存。
(4)waiting to lock <地址> 目标
说明线程使用synchronized申请对象锁未成功,于是开始等待别的线程释放锁。线程在监视器的进入区等待。这条一般在调用栈顶出现,线程状态一般对应为Blocked。
2.TIMED_WAITING
如下图所示,为使用jstack工具dump线程后,查看到的线程处于TIMED_WAITING状态。
(1)线程的状态是:TIMED_WAITING
这时的线程处于sleep状态,说明线程在有时限的等待另一个线程的特定操作,一般会有超时时间唤醒。就一般情况来说,出现TIMED_WAITING很正常,等待网络IO等都会出现这种状态,但是大量的线程处于TIMED_WAITING时,需要我们重点分析。
(2)第一行中,显示线程在waiting on condition,这说明线程在等待某个条件的发生,从而自己唤醒,或者是调用了 sleep(n)。
在我们这个例子里,线程处于 TIMED_WAITING状态。
(3)parking to wait for <地址> 目标
这里即为第一行“waiting on condition”所等待的条件,等待是java.util.concurrent.CountDownLatch$Sync,这是一种闭锁的实现,是一种同步工具类,可以延迟线程的进度直到闭锁到达终止状态,其内部包含一个计数器,该计数器被初始化为一个整数,表示需要等待事件的数量。由以上分析可以知道,线程是因为向druid写数据,由于有同步机制,而进入TIMED_WAITING状态。
(4)和上个例子线程在parking to wait for 不同,在这个例子中,线程也是处于TIMED_WAITING状态,但是第一行中显示线程正在 in Object.wait(),第四行显示线程waiting on <地址> 目标。
本例中线程就处于TIMED_WAITING状态。
3.WAITING
如下图所示,为使用jstack工具dump线程后,查看到的线程处于WAITING状态。
(1)线程的状态是:WAITING
意思就是线程在等待另外一个线程去解除它的等待状态。一个典型的例子就是生产者消费者模型,当生产者生产太慢的时候,消费者要等待生产者生产才能去消费,这段时间消费者线程就处于waiting状态。还可以使用lock.wait()方法使线程进入waiting状态,无超时的等待,必须等待lock.notify()或lock.notifyAll()或接收到interrupt信号才能退出等待状态。
(2)parking to wait for <地址> 目标
第一行中,显示线程在waiting on condition,这说明线程在等待某个条件的发生,从而自己唤醒。
在这个例子里,线程处于 WAITING状态,parking to wait for所等待的是java.util.concurrent.locks.AbstractQueuedSynchronizer,这也是java实现同步机制。
4.RUNNABLE
如下图所示,为使用jstack工具dump线程后,查看到的线程处于RUNNABLE 状态。
在这个例子里,可以清楚看到整个线程运行的过程。在线程运行过程中,有很多次获取锁,即为上图中locked <地址> 目标,即此线程使用synchronized申请对象锁成功,是监视器的拥有者,可以在临界区内进行操作。上图所lock的内容有java IO的输入输出流等。
-----------借鉴部分结束/
三、企业云在线上测试过程中,通过线程打印有了一个意外收获
如下面信息,“http-bio-18272-exec-258”,表示Tomcat 的启动模式为 bio模式,将bio模式改为nio模式,在企业云项目中,其他条件不变,只将bio模式更改为nio模式,tps提升了一倍
"http-bio-18272-exec-258" daemon prio=10 tid=0x0000000003308800 nid=0x3c29 waiting on condition [0x00007fa01c122000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x0000000706034fa8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)
at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:86)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
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。
本文来自网易实践者社区,经作者张子铎授权发布。