本文共 3855 字,大约阅读时间需要 12 分钟。
线程堆栈也称线程调用堆栈,是虚拟机中线程(包括锁)状态的一个瞬间快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。借助堆栈信息可以帮助分析很多问题,如线程死锁,锁争用,死循环,识别耗时操作等等。在多线程场合下的稳定性问题分析和性能问题分析,线程堆栈分析湿最有效的方法,在多数情况下,无需对系统了解就可以进行相应的分析。如下类型问题:
借助线程堆栈可以帮助我们缩小范围,找到突破口。线程堆栈分析很多时候不需要源代码,在很多场合都有优势。下面我们就开始我们的线程堆栈之旅。
示例:$jstack –l 23561 >> xxx.dump
命令 : $jstack [option] pid >> 文件
>>表示输出到文件尾部,实际运行中,往往一次dump的信息,还不足以确认问题,建议产生三次dump信息,如果每次dump都指向同一个问题,我们才确定问题的典型性。
如下面一段java源代码程序:
通过上节的介绍的方法打印堆栈信息,我们只关注java用户线程,其他由虚拟机自动创建的,在实际分析中,只关心java用户线程即可。
[java]从上面的main线程看,线程堆栈里面的最直观的信息是当前线程的调用上下文,即从哪个函数调用到哪个函数(从下往上看),正执行到哪一类的哪一行,借助这些信息,我们就对当前系统正在做什么一目了然。
另外,从main线程的堆栈中,有-locked<0xc8c1a090>(a java.lang.Object) 语句,这表示该线程已经占用了锁,其中0xc8c1a090表示锁ID,这个锁ID是系统自动生成的,我们只需要知道每次打印堆栈,同一个ID表示是同一个锁即可
其中"线程对应的本地线程Id号"所指的本地线程是指该java虚拟机所对应的虚拟机中的本地线程,我们知道java是解析型语言,执行的实体是java虚拟机,因此java代码是依附于java虚拟机的本地线程执行的,之前文章中讲过,当启动一个线程时,是创建一个native本地线程,本地线程才是真实的线程实体,为了更加深入理解本地线程和java线程的关系,我们可以通过以下方式将java虚拟机的本地线程打印出来:
1、试用ps -ef|grep java 获得java进行id
2、试用pstack<java pid> 获得java虚拟机本地线程的堆栈
从操作系统打印出来的虚拟机的本地线程看,本地线程数量和java线程数量是相同的,说明二者是一一对应的关系。
我们获取的本地线程堆栈如下:
这个本地线程号如何与java线程堆栈文件对应起来呢,每一个线程都有tid,nid的属性,通过这些属性可以对应相应的本地线程,我们先看java线程第一行,里面有一个属性是nid,
main" prio=1 tid=0x0805c988 nid=0xd28 runnable [0xfff65000..0xfff659c8]
其中nid是native thread id,也就是本地线程中的LWPID,二者是相同的,只不过java线程中的nid用16进制表示,本地线程的id用十进制表示。3368的十六进制表示0xd28,在java线程堆栈中查找nid为0xd28就是本地线程对应的java线程。
wait和sleep的重要区别。wait和sleep有一个共同点,就是二者都把当前线程阻塞住,我们叫睡眠或等待,二者有着本质区别:
在wait(5000)这个期间,当前线程会释放它占用的锁,其他线程有机会获得到该锁,当wait(5000)结束后,当前线程继续获取该锁的使用权。满足以下条件之一,wait退出:
1、达到等待时间之后,自动退出
2、其他线程调用了该锁的notify方法,如果多个线程在等待同一个锁,只有一个线程会被通知到。
sleep方法是线程的一个静态方法,实际上和锁操作无关,不会产生特别的锁,如果原来持有,现在仍然持有,如果原来没有,现在仍然没有。
从上面介绍的线程堆栈看,线程堆栈中包含直接信息为:线程个数,每个线程调用的方法堆栈,当前锁的状态。从线程个数可以直接数出来,线程调用的方法堆栈,从下向上看,表示了当前线程调用哪个类哪个方法,锁的状态看起来需要一些技巧,与锁相关的重要信息如下:
在线程堆栈中与锁相关的三个最重要的特征字:locked,waiting to lock,waiting on 了解这三个特征字,就可以对锁进行分析了。
一般情况下,当一个或一些线程正在等待一个锁的时候,应该有一个线程占用了这个锁,即如果有一个线程正在等待一个锁,该锁必然被另一个线程占用,从线程堆栈中看,如果看到waiting to lock<0x22bffb60>,应该也应该有locked<0x22bffb60>,大多数情况下确实如此,但是有些情况下,会发现线程堆栈中可能根本没有locked<0x22bffb60>,而只有waiting to ,这是什么原因呢,实际上,在一个线程释放锁和另一个线程被唤醒之间有一个时间窗,如果这个期间,恰好打印堆栈信息,那么只会找到waiting to ,但是找不到locked 该锁的线程,当然不同的JAVA虚拟机有不同的实现策略,不一定会立刻响应请求,也许会等待正在执行的线程执行完成。
借助线程堆栈信息,可以分析很多问题,其中cpu的消耗分析也是线程堆栈分析的一个重要内容。java线程状态有以下几类:
处于RUNNABLE状态的线程是不是一定会消耗cpu呢,不一定,像socket IO操作,线程正在从网络上读取数据,尽管线程状态RUNNABLE,但实际上网络io,线程绝大多数时间是被挂起的,只有当数据到达后,线程才会被唤起,挂起发生在本地代码(native)中,虚拟机根本不一致,不像显式的调用sleep和wait方法,虚拟机才能知道线程的真正状态,但在本地代码中的挂起,虚拟机无法知道真正的线程状态,因此一概显示为RUNNABLE。
总结:
如果是本地代码,集合本地代码的性质,可以通过pstack获取本地的线程堆栈,如果是纯运算代码,则消耗cpu,如果被挂起,则不消耗,如果是io,则不怎么消耗cpu。