线程堆栈也称作线程调用堆栈。Java线程堆栈是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况等信息。对于已经消失而又没留有痕迹的信息,线程堆栈是无法进行历史追踪的。
线程堆栈的信息包括:
(1)线程的名字,id,线程的数量等
(2)线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程再等待锁等)
(3)调用堆栈(即函数的调用层次关系),调用堆栈包括完整的类名,所执行的方法,源代码的行数。
借助线程堆栈可以分析:线程死锁、锁争用、死循环、识别耗时操作、稳定性分析和性能分析等问题
线程作用:
因为线程栈是瞬时快照包含线程状态以及调用关系,所以借助堆栈信息可以帮助分析很多问题,比如线程死锁,锁争用,死循环,识别耗时操作等等。线程栈是瞬时记录,所以没有历史消息的回溯,一般我们都需要结合程序的日志进行跟踪,一般线程栈能分析如下性能问题:
1、系统无缘无故的cpu过高 2、系统挂起,无响应 3、系统运行越来越慢 4、性能瓶颈(如无法充分利用cpu等) 5、线程死锁,死循环等 6、由于线程数量太多导致的内存溢出(如无法创建线程等)
比如有如下堆栈实例:
"resin-22129" daemon prio=10 tid=0x00007fbe5c34e000 nid=0x4cb1 waiting on condition [0x00007fbe4ff7c000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:315)
at com.caucho.env.thread2.ResinThread2.park(ResinThread2.java:196)
at com.caucho.env.thread2.ResinThread2.runTasks(ResinThread2.java:147)
at com.caucho.env.thread2.ResinThread2.run(ResinThread2.java:118)
"Timer-20" daemon prio=10 tid=0x00007fe3a4bfb800 nid=0x1a31 in Object.wait() [0x00007fe3a077a000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000006f0620ff0> (a java.util.TaskQueue)
at java.util.TimerThread.mainLoop(Timer.java:552)
- locked <0x00000006f0620ff0> (a java.util.TaskQueue)
at java.util.TimerThread.run(Timer.java:505)
- “resin-22129” 线程名称。
- daemon 线程类型:线程分为守护线程 (daemon) 和非守护线程 (non-daemon) 两种;
- prio=10 线程优先级:默认为5,数字越大优先级越高。
- tid=0x…000 JVM线程的id,通过Thread.getId()获取。
- nid=0x4cb1 系统线程id:对应的系统线程id,可以通过 top命令进行查看的pid对应,线程id是十六进制的形式;
- waiting on condition 操作系统线程状态:具体含义见下面分析。
- [0x00007fbe4ff7c000] 起始栈地址:线程堆栈调用的起始内存地址;
- java.lang.Thread.State: WAITING (parking) JVM线程状态:具体见下面分析。
- 线程此时正在执行的方法,以及调用链
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:315)
at com.caucho.env.thread2.ResinThread2.park(ResinThread2.java:196)
(1)deadlock 死锁线程,一般指多个线程调用期间进入了相互资源占用,导致一直等待无法释放的情况。 (2)runnable 一般指该线程正在执行状态中,该线程占用了资源。 (3)blocked 线程正处于阻塞状态,指当前线程执行过程中,所需要的资源长时间等待却一直未能获取到,被容器的线程管理器标识为阻塞状态,可以理解为等待资源超时的线程。 (4)waiting on condition 线程正处于等待资源或等待某个条件的发生。 (5)waiting for monitor entry 或 in Object.wait() java内置锁(synchronized)jvm线程状态
(1)NEW 线程刚刚被创建,也就是已经new过了,但是还没有调用start()方法,这个状态我们使用jstack进行线程栈dump的时候基本看不到,因为是线程刚创建时候的状态。 (2)RUNNABLE 从虚拟机的角度看,线程正在运行状态,状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等。 处于RUNNABLE状态的线程是不是一定会消耗cpu呢,不一定,像socket IO操作,线程正在从网络上读取数据,尽管线程状态RUNNABLE,但实际上网络io,线程绝大多数时间是被挂起的,只有当数据到达后,线程才会被唤起,挂起发生在本地代码(native)中,虚拟机根本不一致,不像显式的调用sleep和wait方法,虚拟机才能知道线程的真正状态,但在本地代码中的挂起,虚拟机无法知道真正的线程状态,因此一概显示为RUNNABLE。 (3)BLOCKED 线程处于阻塞状态,正在等待一个monitor lock。 通常情况下,是因为本线程与其他线程公用了一个锁。其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法, 而本线程进入这个同步代码块也需要这个锁,最终导致本线程处于阻塞状态。 真实生活例子: 今天你要去阿里面试。这是你梦想的工作,你已经盯着它多年了。你早上起来,准备好,穿上你最好的外衣,对着镜子打理好。 当你走进车库发现你的朋友已经把车开走了。在这个场景,你只有一辆车,所以怎么办?在真实生活中,可能会打架抢车。 现在因为你朋友把车开走了你被BLOCKED了。你不能去参加面试。 这就是BLOCKED状态。用技术术语讲,你是线程T1,你朋友是线程T2,而锁是车。T1被BLOCKED在锁(例子里的车)上,因为T2已经获取了这个锁。 (4)WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束,处于waiting状态的线程基本不消耗CPU。 小贴士:当线程调用以下方法时会进入WAITING状态 1,不带超时的 Object.wait 方法。 2,不带超时的 Thread.join 方法。 3,LockSupport.park 方法。 (5)TIMED_WAITING 该线程正在等待,通过使用了 sleep, wait, join 或者是 park 方法。(这个与 WAITING 不同是通过方法参数指定了最大等待时间,WAITING 可以通过时间或者是外部的变化解除),线程等待指定的时间。 真实生活例子: 尽管这次面试过程充满戏剧性,但你在面试中做的非常好,惊艳了所有人并获得了高薪工作。 你回家告诉你的邻居你的新工作并表达你激动的心情。你的朋友告诉你他也在同一个办公楼里工作。他建议你坐他的车去上班。 你想这不错。所以去阿里上班的第一天,你走到你邻居的房子,在他的房子前停好你的车。你等了他10分钟,但你的邻居没有出现。 你然后继续开自己的车去上班,这样你不会在第一天就迟到。这就是TIMED_WAITING. 用技术术语来解释,你是线程T1而你的邻居是线程T2。你释放了锁(这里是停止开车)并等了足足10分钟。如果你的邻居T2没有来,你继续开车(老司机注意车速,其他乘客记得买票)。 小贴士:当线程调用以下方法时会进入TIMED_WAITING状态 1,Thread.sleep 方法。 2,指定超时值的 Object.wait 方法。 3,指定超时值的 Thread.join 方法。 4,LockSupport.parkNanos。 5,LockSupport.parkUntil。 (6)TERMINATED 线程处于终止状态。同样我们在使用jstack进行线程dump的时候也很少看到该状态的线程栈。
状态小结:
这些状态中NEW状态是开始,TERMINATED是销毁,在整个线程对象的运行过程中,这个两个状态只能出现一次。其他任何状态都可以出现多次,彼此之间可以相互转换
-
处于timed_waiting/waiting状态的线程一定不消耗cpu,处于runnable状态的线程不一定会消耗cpu,要结合当前线程代码的性质判断,是否消耗cpu
-
如果是纯java运算代码,则消耗cpu
-
如果线程处于网络io,很少消耗cpu
-
如果是本地代码,通过查看代码,可以通过pstack获取本地的线程堆栈,如果是纯运算代码,则消耗cpu,如果被挂起,则不消耗,如果是io,则不怎么消耗cpu
以下是状态转化图,可以较为清晰地看到状态转换的场景与条件:
线程栈中包含直接信息为:线程个数,每个线程调用的方法堆栈,当前锁的状态。从线程个数可以直接数出来,线程调用的方法堆栈,从下向上看,表示了当前线程调用哪个类哪个方法,锁的状态看起来需要一些技巧,与锁相关的重要信息如下:
- 当一个线程占有一个锁的时候,线程堆栈会打印一个-locked<0x22bffb60>
- 当一个线程正在等在其他线程释放该锁,线程堆栈会打印一个-waiting to lock<0x22bffb60>
- 当一个线程占有一个锁,但又执行在该锁的wait上,线程堆栈中首先打印locked,然后打印-waiting on <0x22c03c60>
在线程堆栈中与锁相关的三个最重要的特征字:locked,waiting to lock,waiting on 了解这三个特征字,就可以对锁进行分析了。
一般情况下,当一个或一些线程正在等待一个锁的时候,应该有一个线程占用了这个锁,即如果有一个线程正在等待一个锁,该锁必然被另一个线程占用,从线程堆栈中看,如果看到waiting to lock<0x22bffb60>,应该也应该有locked<0x22bffb60>,大多数情况下确实如此,但是有些情况下,会发现线程堆栈中可能根本没有locked<0x22bffb60>,而只有waiting to ,这是什么原因呢,实际上,在一个线程释放锁和另一个线程被唤醒之间有一个时间窗,如果这个期间,恰好打印堆栈信息,那么只会找到waiting to ,但是找不到locked 该锁的线程,当然不同的JAVA虚拟机有不同的实现策略,不一定会立刻响应请求,也许会等待正在执行的线程执行完成。



