(接上文《JVM调试常用命令——jstack命令与Java线程栈(1)》)
上一篇文章中我们介绍了jstack命令的基本使用,也列举了一个比较简单的示例。虽然之前的文章内容中没有介绍查询结果中的一些关键信息,但是这并不影响什么。本片文章中我们将结合之前讲过的线程状态切换,对jstack命令的结果进行讲解。
方法栈实际上是线程栈原子对象“栈帧”中的信息概要(“栈帧”内容将在后文中进行介绍),如下内容片段图例中,是一部分连续的方法栈信息:
1、这是一个名叫http-nio-5700-Acceptor-0的线程(这个线程是笔者从Spring boot运行实例上截取的,用于使用NIO模型接收5700端口上的Http请求),当前这个线程处于RUNNABLE可运行状态(为什么是可运行,而不是运行状态呢?这个问题将在后文中解答)。
2、上文已经提到线程栈dump信息中,有个重要信息就是方法栈信息,方法栈实际上是“栈帧”信息的概要,它以“at”关键字开头。例如上图示例中,该线程的方法调用过程在Thread类中的第748行开始调用另一个方法(看过源代码的朋友可以知道,第748行代码就是执行Thread中target对象——一个实现了Runnable接口实例的run()方法)。紧接着在NioEndpoint$Acceptor类中的第453行(既是在NioEndpoint$Acceptor.run方法中),调用下一个方法。
3、请注意在方法栈中的第三个方法调用过程中,该线程持有了一个对象的操作权(俗称“锁”),这个对象是一个“java.lang.Object”类的实例。那么除非这个对象通过某种方式释放掉这个对象的操作权,否则一旦其它线程(记为B)需要操作这个对象,并试图获得这个对象的“锁”时,这个线程B就会进入“阻塞”状态。接着我们再看一个阻塞状态的线程栈实例,如下图所示:
这是一个名为"http-nio-5700-exec-1"的线程,实际上这是“http-nio-5700-Acceptor-0”线程拿到准备好后的http处理请求后,紧接着实际进行http请求处理的所谓“执行线程”。这个线程方法栈底部的方法区就不逐一进行介绍了,熟悉java原生线程池的朋友都可以看到,这明显是将处理任务送入了一个java线程池中然后进出处理(另外还可以看到这个线程池所使用的任务队列是LinkedBlockingQueue)。
紧接着我们注意到,当前线程处于阻塞状态。阻塞点在哪里呢?请注意方法栈栈顶的两个方法:当前线程在执行LockSupport类的第175行时(在LockSupport.park方法中),调用了一个JNI方法“Unsafe.park”,在这个后者内部当前线程等待内存地址起始点为“0x00000007853d5748”的对象的操作权限——这个对象是AbstractQueuedSynchronizer$ConditionObject类的一个对象。于是线程进入阻塞状态。
最后我们再看一个更简单的,让线程进入“阻塞”状态的实例:
很显然,以上线程进入了阻塞状态——而且是一个有时间约束的阻塞。原因是当前线程在执行“AbstractProtocol$AsyncTimeout”类中的第1200行时(在AbstractProtocol$AsyncTimeout.run方法体内),调用了“Thread.sleep”方法。
是的,通过以上小节的讲解我们知道了,线程状态是线程dump信息中非常重要的信息项。并且仿佛根据进入阻塞的原因(调用方式)不一样,同样的“阻塞”状态都还有一些细小的差别。为了能够看懂jstack命令给出的线程栈dump信息中线程的状态描述,我们需要首先介绍一下线程的状态分类表述。
RUNNABLE, 在虚拟机内执行的。运行中状态。注意,处于这样的线程可能在其方法栈中还能看到locked关键字,这表明它获得了某个对象的操作权限,并继续进行着后续的处理过程。
BLOCKED, 试图获得指定对象的操作权限,并进入synchronized同步块,但是由于某种原因当前线程还没有获得指定的操作操作权限,还在synchronized同步块外处于阻塞状态。
WATING, 一种无限期阻塞状态,等待另一个线程执行特定操作。等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。根据进入的方式不一样,阻塞状态下还有一些细微的差别,这个问题我们将在下一节进行详细讲解。这里特别要注意BLOCKED和WATING/TIMED_WATING的区别,这个区别将在后文进行详细讲解
TIMED_WATING, 有时限的等待另一个线程的特定操作。例如,和WAITING的区别可能是wait() 等语句加上了时间限制 wait(timeout),也可能是调用了sleep方法。
TERMINATED,线程已退出的。
实际上以上的几个状态在Java源代码中都已经做了非常详细的说明,如下所示:
这是因为线程和线程调度都是操作系统级别的概念,某一个线程是否由CPU进行运行,是无法由开发者、应用程序使用者决定的,甚至不是由JVM决定的(JVM只对线程优先级、线程调度类型的选择提供支持),而是由操作系统决定。而由于Java的跨平台性,所以在JVM中有专门的线程调度程序(模块)来配合不同操作系统中线程的调用方式完成线程调度。作为JVM来说只能通过Native的方式将线程状态变更为“可运行”(包括设定优先级),然后由操作系统来决定具体运行哪一个线程。所以JVM中对于处理运行状态的线程,其标识都是“RUNNABLE”。
当用new操作符创建一个线程时, 例如new Thread®。这时线程还没有运行,那么它就处于新建状态。新建状态不能通过jstack命令进行跟踪,原因是当前线程还没有由托管到操作系统,但是我们可以通过Java代码调出当前线程的状态信息,如下图所示:
可以看到主线程还没有执行thread1.start()方法,这时打印的线程状态信息就是:
当前threa1线程状态为:NEW
当前threa2线程状态为:NEW
上文已经提到当某一个线程需要得到指定的Object的操作权,并试图进入synchronized同步块,但发现不能获得Object的操作权时,就会进入BLOCKED状态。请看如下代码和调试效果:
上图中有创建了两个用户线程,名称分别为“thread1”和“thread2”。因为其中thread2先于thread1拿到了TestStates.class这个对象的操作权(基础知识:class也是对象),并进入synchronized同步块(参见调试信息);所以thread1进入BLOCKED状态,我们使用jstack信息观察当前Java进程线程栈的状态可发现如下图所示的情况:
# jstack 197300
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000002fce000 nid=0x3b794 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"thread2" #15 prio=5 os_prio=0 tid=0x000000001ee53000 nid=0x368d8 runnable [0x000000001febf000]
java.lang.Thread.State: RUNNABLE
at testThread.TestStates$MyThread.run(TestStates.java:23)
- locked <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
at java.lang.Thread.run(Thread.java:748)
"thread1" #14 prio=5 os_prio=0 tid=0x000000001ee52000 nid=0x3b80c waiting for monitor entry [0x000000001fdbf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at testThread.TestStates$MyThread.run(TestStates.java:23)
- waiting to lock <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
at java.lang.Thread.run(Thread.java:748)
.........后面的信息省去
可以看到名叫thread1的线程正在等待获取对象(0x000000076c5c4090)的操作权限,后者是一个java.lang.Class类的实例。而目前持有当前对象(0x000000076c5c4090)操作权限的线程是名为thread2的线程。
线程从可运行状态切换到WAITING状态的方式就很多了。例如:调用wait方法,释放对象操作权、当前线程调用目前线程的join方法,等待后者执行完成、当前线程调用sleep方法、可重入锁(包括读写分离锁)调用lock方法,并触发阻塞效果、调用LockSupport的park方法,自旋当前线程或指定的对象等等,下文我们将要对这些细节进行详细描述。
首先介绍一组最简单的方式,就是当前在synchronized同步块中的线程调用wait()方法,让出被锁定对象的操作权限。如下示例:
在以上代码中,虽然线程thread1先于thread2拿到TestStates.class对象的操作权,并唯一进入synchronized同步块。但是在synchronized同步块中,线程thread1调用了wait方法让出了操作权。这时还未进图synchronized同步块的线程thread2就可以拿到操作权(因为在synchronized同步块外只有thread2再等待获得资源的操作权)。这时线程thread1就进入了WAITING状态,而thread2由BLOCKED状态变成了Runnable状态。通过jstack命令,我们可以验证到这样的状态切换:
# jstack 246124
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
"thread2" #15 prio=5 os_prio=0 tid=0x000000001ef74800 nid=0x3c554 runnable [0x000000001ffdf000]
java.lang.Thread.State: RUNNABLE
at testThread.TestStates$MyThread.run(TestStates.java:22)
- locked <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
at java.lang.Thread.run(Thread.java:748)
"thread1" #14 prio=5 os_prio=0 tid=0x000000001ef73800 nid=0x3c5cc in Object.wait() [0x000000001fedf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
at java.lang.Object.wait(Object.java:502)
at testThread.TestStates$MyThread.run(TestStates.java:24)
- locked <0x000000076c5c4090> (a java.lang.Class for testThread.TestStates)
at java.lang.Thread.run(Thread.java:748)
通过jstack命令我们观察到:线程thread1的线程栈中,从栈底开始的第二个栈帧拿到了对象(0x000000076c5c4090)的操作权限,并执行到TestStates$MyThread类中的第24行(既是在run方法中)。接着在24行调用了Object类中的wait方法(Object类中的第502行),最后通过调用JNI中的wait方法,重新等待获取对象(0x000000076c5c4090)的操作权。
而线程thread2拿到了对象(0x000000076c5c4090)的操作权,并正在执行TestStates$MyThread类中的第22行代码(同样在TestStates$MyThread类的run方法中,既是那句System.out)。这里需要特别注意,当正在运行的线程由于操作系统的原因运行非常“卡顿”,例如操作系统的磁盘I/O操作、操作系统的网络I/O操作,那么我们通过jstack命令观察时,还是会发现当前线程处于“RUNNABLE”状态。
请注意这个WAITING状态的备注说明“java.lang.Thread.State: WAITING (on object monitor)”,这非常关键,这告诉我们该线程进入WAITING状态的原因是对象监视器造成——具体来说是对象监视器发现当前线程thread1已经让出了操作权限。
=================================
(接下文)