爱分享666

当前位置:首页 >java技术博客>java基础

Java线程分析及理解

时间:2019-07-14 04:30:41 阅读:307

资源免费下载

JDK本身提供了很多方便的JVM性能调优监控工具,除了集成式的VisualVM和jConsole外,还有jps、jstack、jmap、jhat、jstat等小巧的工具,另外还有一些商业性的工具,如JProfiler等,相对于这些集成性的工具,在资源消耗方面会比较大,有时候一些线上临时性的需要抓取线程信息和堆信息的情况,这些集成性的工具使用起来就显得不“那么强大和好用了”,这个时候,反而一些简单的命令,可以帮我们解决燃眉之急,也非常简单好用,这次就先简单介绍一下线程相关的知识,以及常用工具的用法,帮助我们快速定位问题。

一、 首先,先了解一下java线程的几种状态:

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

线程等待指定的时间,对于以下方法的调用,可能会导致线程处于这个状态:

  • Thread.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线程状态主要是以下几种:

  • RUNNABLE 线程运行中或I/O等待
  • BLOCKED 线程在等待monitor锁(synchronized关键字)
  • TIMED_WAITING 线程在等待唤醒,但设置了时限
  • WAITING 线程在无限等待唤醒

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

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()”。
        目前线程状态为:waiting for monitor entry,说明它是“Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。
这时有两种可能性:
     该 monitor不被其它线程拥有, Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码
     该 monitor被其它线程拥有,本线程在 Entry Set队列中等待。 

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


根据以上分析,我们可以看出,线程想要调用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)。

当线程在waiting on condition时,线程状态可能为
  • java.lang.Thread.State: WAITING (parking):一直等某个条件发生;
  • java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定时等待某个条件发生,即使这个条件不到来,也将定时唤醒自己。

在我们这个例子里,线程处于 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 <地址> 目标

线程在in Object.wait(), 说明线程在获得了监视器之后,又调用了 java.lang.Object.wait() 方法。
如1(3)中所说,等待monitor 的线程分为两种,在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。1(3)中的例子是在 “Entry Set”中等待的线程。而本例是在“Wait Set”中等待的线程,其状态是in Object.wait()
这说明线程获得了 Monitor,但是线程继续运行的条件没有满足,则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。
此时线程状态大致为以下几种:
  • java.lang.Thread.State: TIMED_WAITING (on object monitor);
  • java.lang.Thread.State: WAITING (on object monitor);

本例中线程就处于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 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实现同步机制。

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。

本文来自网易实践者社区,经作者张子铎授权发布。 

上一篇:

CAS无锁实现原理、概念以及出现ABA问题原因

下一篇:

Vue.js零基础入门到项目实战_Vue.js_视频教程_配套源程序

0 +1
打赏 ×

如果网站能给予您帮助,欢迎给网站捐助,给我打赏个吧!
您的支持是我的动力,让网站能一直陪伴着大家,共同学习进步。
捐助费用将用于网站日常运营(服务器租费、域名租费等)
捐助者请发送邮箱提供姓名至 zhaoqn@163.com 留言以表感谢。

网友评论


  • 验证码:

热门评论

本月热门

推荐资料

精彩评论

回到顶部