您的位置:首页 > 编程语言 > Java开发

Java线程系列(1)——thread dump格式、锁与线程的状态

2016-12-05 21:20 375 查看

Java线程系列(1)——thread dump格式、锁与线程的状态

前不久连续收到Java线程数量过多的报警, 通过 Jstack 工具导出生产环境服务器的线程快照后, 通过分析 dump 文件, 很快就确定了问题。 以前没有排查线上线程数量问题的实战经验, 因此想借助这次机会, 从 thread dump 的角度重新认识Java线程。

本文是Java线程系列文章的第一篇, 主要内容如下:

Jstack用法, 以及容易踩到的坑;

thread dump文件的内容解析;

线程状态与Java Monitor;

线程状态实例解析;

一、Jstack 用法

Jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。

学习一个Java命令, 最好的方式就是看Java文档, 使用
jstack -help
看下帮助文档:

~ jstack -help
Usage:
jstack [-l] <pid>
(to connect to running process)
jstack -F [-m] [-l] <pid>
(to connect to a hung process)
jstack [-m] [-l] <executable> <core>
(to connect to a core file)
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server)

Options:
-F  to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m  to print both java and native frames (mixed mode)
-l  long listing. Prints additional information about locks
-h or -help to print this help message


1. 简单说明

-F
: 当执行
jstack <pid>
没有响应的时候, 加上
-F
参数可以强制导出 dump 文件.

-m
: 同时打印Java虚拟机栈和本地方法栈信息.

-l
: 打印关于锁的附加信息, 例如属于 java.util.concurrent 的 ownable synchronizers 列表.

2. 容易踩到的坑

在Linux服务器上, 我们经常采用
Jps
工具和
ps aux | grep java
命令来查看Java进程的 PID, 但有时候可能会遇到
ps aux | grep java
命令可以找到Java进程, 但是
Jps
却找不到该进程。 磁盘满了、用户没有写PID文件的权限或者读权限都会导致这个现象出现, 但是最有可能的原因是启动Java进程的用户和当前执行Jps命令的用户不是同一个, 因为Jps工具只能显示当前用户的Java进程。Jps介绍以及解决jps无法查看某个已经启动的java进程问题

在Linux服务器上, 使用
jstack <pid>
无法导出 dump 文件, 加上
-F
参数依然无法导出 dump 文件。很可能的原因是执行jstack命令的用户和启动java进程的用户不是同一个, 切换到启动java进程的用户上执行jstack命令, 可以轻松导出dump文件。

二、thread dump 文件

thread dump 是java虚拟机的线程快照, 包含每个线程当前时刻的一系列状态和执行信息。了解和熟悉dump文件的格式内容, 有助于分析和解决问题。dump文件中, 每个线程基本都一样, 下面随便选取一个分析下:

"haha@419" prio=5 tid=0xb nid=NA sleeping
java.lang.Thread.State: TIMED_WAITING
blocks hehe@420
at java.lang.Thread.sleep(Thread.java:-1)
at liyin.code.App$1.run(App.java:13)
- locked <0x19f> (a java.lang.Class)
at java.lang.Thread.run(Thread.java:745)


1. 文件解析:

haha@419
: 它是线程的名字, 可以在new Thread()对象的时候指定。有了名称,搜索thread dump的时候更加方便, 如果名字命名得好, 看见名字就可以清楚得知道该线程在业务中做什么事情, 可以缩短排查问题的时间。
好的习惯: 代码中任何创建线程的地方都应该给线程取一个有意义的名字, 使用线程池时同样需要取一个有意思的名字
.

prio
: 它代表的是线程的优先级priority,也可以通过Thread类中的 setPriority 修改.

tid
: Java的线程Id (这个线程在当前虚拟机中的唯一标识).

nid
: 线程本地标识,是线程在操作系统中的标识.

sleeping
: 线程当前的行为信息, 此线程当前正处于sleeping状态。类似的还有
in Object.wait()
waiting for monitor entry
等, 每一个行为状态都代表java线程当前的执行情况.

java.lang.Thread.State
: 它标识了线程的当前状态, 当前的线程正处于
有限的时间等待
, 因为当前线程sleep完成后就会继续执行.

@ xxx
: 从最下面的 @ 开始往上是线程的调用栈, 通过它可以看出线程执行代码的流程.

- locked
: 它是线程调用修饰, 即线程执行的额外信息。
- locked <0x19f> (a java.lang.Class)
代表当前线程获取到了内存地址为
0x19f
对象的锁, 该对象是一个
java.lang.Class
类的具体实例, 即当前线程获取到了一个类的字节码对象的锁。类似的修饰符还有
waiting to lock
waiting on
等, 它们代表了线程继续执行下去的额外必须操作.

2. 调用信息

本线程调用栈信息: 名为
haha@419
的线程从
java.lang.Thread.run
方法开始执行, 它获取到了
0x19f
对象的锁后, 并调用了
liyin.code.App
的第一个内部类的
run
方法, 在
liyin.code.App$1
的run方法内部, 该线程又调用了
java.lang.Thread
类的
sleep
静态方法, 该线程持有锁并睡眠进入有限时间等待状态。

三、线程状态与锁

1. 线程状态

Jstack分析dump之前, 必须要了解线程的状态。dump文件中, 每个线程的信息的第二行
java.lang.Thread.State: XXX
就标识了当前线程的状态。

线程的状态在Thread.State这个枚举类型中定义:

/**
* A thread can be in only one state at a given point in time.
* These states are virtual machine states which do not reflect
* any operating system thread states.
*/
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,

/**
* Thread state for a runnable thread.  A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,

/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
*/
BLOCKED,

/**
* Thread state for a waiting thread.
* A thread in the waiting state is waiting for another thread to
* perform a particular action.
*/
WAITING,

/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
*/
TIMED_WAITING,

/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}


Java线程在某个时刻必定只能是
Thread.State
枚举类型中的一个, 这些状态信息只反应线程在Java虚拟机中的状态, 并不反应线程在操作系统中的状态信息。具体状态代表的信息如下:

*
NEW
: 线程未启动。每一个线程,在堆内存中都有一个对应的Thread对象。Thread t = new Thread(); 当刚刚在堆内存中创建Thread对象,还没有调用t.start()方法之前,线程就处在NEW状态。在这个状态上,线程与普通的Java对象没有什么区别,就仅仅是一个堆内存中的对象。

*
RUNNABLE
: 该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。 这个状态的线程比较正常,但如果线程长时间停留在在这个状态就不正常了,这说明线程运行的时间很长(存在性能问题),或者是线程一直得不得执行的机会(存在线程饥饿的问题)。

*
BLOCKED
: 线程正在等待获取java对象的监视器(也叫内置锁),即线程正在等待进入由synchronized保护的方法或者代码块。

*
WAITING
: 处在该线程的状态,正在等待某个事件的发生,只有特定的条件满足,才能获得执行机会。而产生这个特定的事件,通常都是另一个线程。也就是说,如果不发生特定的事件,那么处在该状态的线程一直等待,不能获取执行的机会。

*
TIMED_WAITING
: TIMED_WAITING意味着线程调用了限时版本的API,正在等待时间流逝;当等待时间过去后,线程一样可以恢复运行。如果线程进入了WAITING状态,一定要特定的事件发生才能恢复运行;而处在TIMED_WAITING的线程,如果特定的事件发生或者是时间流逝完毕,都会恢复运行。

*
TERMINATED
: 线程执行完毕,执行完run方法正常返回,或者抛出了运行时异常而结束,线程都会停留在这个状态。这个时候线程只剩下Thread对象了,没有什么用了。

2. Java Monitor

在多线程的Java程序中,Monitor是Java中用以实现线程之间的互斥与协作的主要手段。每一个对象都有且有一个Monitor。下图描述了线程和Monitor之间关系,以及线程的状态转换图:



进入区(Entrt Set)
: 表示线程通过synchronized要求获取对象的锁。如果对象未被锁住, 则进入拥有者; 否则则在进入区等待。一旦对象锁被其他线程释放, 立即参与竞争。

拥有者(The Owner)
: 表示某一线程成功竞争到对象锁。

等待区(Wait Set)
: 表示线程通过对象的wait方法, 释放对象的锁, 并在等待区等待被唤醒。

从图中可以看出,一个Monitor在某个时刻,只能被一个线程拥有,该线程就是
Active Thread
,而其它线程都是
Waiting Thread
,分别在两个队列
Entry Set
Wait Set
里面等候。在
Entry Set
中等待的线程行为是
waiting for monitor entry
,而在
Wait Set
中等待的线程行为是
in Object.wait()
。 被synchronized保护起来的代码段为临界区, 当一个线程申请进入临界区时,它就进入了
Entry Set
队列。

四、线程状态实例解析

dump文件中线程的状态信息, 透露出了线程发生过的行为。知道了线程的行为, 则有助于我们了解到系统内线程都在做什么, 便于排查和分析问题。下面我将举具体的列子说明线程的行为导致线程产生的状态, 知道了这些关联关系, 有助于以后看见dump文件就知道线程在干什么。

状态为
NEW
的线程未启动, 不会出现在dump文件中,
RUNNABLE
TERMINATED
容易理解就不讨论了,
TIMED_WAITING
比较复杂下篇文章专门讨论。这里主要讨论其他两种:

1. BLOCKED 状态

测试代码:

package liyin.code;

public class App {
public static void main(String[] args) {
Thread haha = new Thread(new Runnable() {
public void run() {
synchronized (App.class) {
while (1 > 0) {
}
}
}
}, "haha");
Thread hehe = new Thread(new Runnable() {
public void run() {
synchronized (App.class) {
System.out.println("i am hehe");
}
}
}, "hehe");
haha.start();
hehe.start();
}
}


一般情况下, haha线程会拿到 App.class 对象的锁, 并一直执行下去, hehe拿不到 App.class 对象的锁会一直阻塞。接下来看下thread dump:

Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):

"hehe" prio=5 tid=0x00007ff00a831800 nid=0x5b03 waiting for monitor entry [0x000070000185e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at liyin.code.App$2.run(App.java:16)
- waiting to lock <0x00000007aab0e788> (a java.lang.Class for liyin.code.App)
at java.lang.Thread.run(Thread.java:745)

"haha" prio=5 tid=0x00007ff00b850800 nid=0x5903 runnable [0x000070000175b000]
java.lang.Thread.State: RUNNABLE
at liyin.code.App$1.run(App.java:8)
- locked <0x00000007aab0e788> (a java.lang.Class for liyin.code.App)
at java.lang.Thread.run(Thread.java:745)


分析:

haha
线程拿到了 App.class 的锁并一直处于执行状态, 所以线程的状态
RUNNABLE
.

hehe
线程为拿到 App.class 的锁进入 App.class 对象的 Entry Set 队列, 线程的行为是
waiting for monitor entry
, 线程的状态是
BLOCKED (on object monitor)
.

hehe
线程的调用修饰
- locked <0x00000007aab0e788>
haha
线程的调用修饰
- waiting to lock <0x00000007aab0e788>
, 锁对象都是
<0x00000007aab0e788>
,直接证明了两个对象之间的锁竞争关系.

BLOCKED
状态表示线程正在等待监视器的锁, 并且超时没有拿到锁线程受到了阻塞。

结论
: 如果thread dump文件中大量出现BLOCKED状态的线程, 代表着系统中锁竞争激烈, 这可能是有问题的, 但要结合具体的业务场景进行分析。如果大量BLOCKED状态的线程都在等一个锁, 那就需要去看看拿着锁的线程正在做什么, 链条式地查找, 很容易就能够找到锁竞争的根源, 但这并不代表程序有问题, 仍然需要结合场景分析。

2. WAITING 状态

测试代码:

package liyin.code;

public class App {
public static void main(String[] args) {

Runnable task = new Runnable() {
public void run() {
synchronized (App.class) {
try {
// 进入等待的同时,会进入释放监视器
App.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};

new Thread(task, "haha").start();
new Thread(task, "hehe").start();
}
}


正常情况下, haha线程和hehe线程会分别拿到 App.class 对象的锁, 并调用 App.class.wait() 方法释放锁后一直阻塞, 直到有另外一个线程调用 App.class.notify 或者 App.class.notifyAll 解除阻塞状态。thread dump 如下:

Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):

"hehe" prio=5 tid=0x00007f90fd00b800 nid=0x5c03 in Object.wait() [0x00007000018e1000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007aab0e868> (a java.lang.Class for liyin.code.App)
at java.lang.Object.wait(Object.java:503)
at liyin.code.App$1.run(App.java:11)
- locked <0x00000007aab0e868> (a java.lang.Class for liyin.code.App)
at java.lang.Thread.run(Thread.java:745)

"haha" prio=5 tid=0x00007f90fc035000 nid=0x5a03 in Object.wait() [0x00007000017de000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007aab0e868> (a java.lang.Class for liyin.code.App)
at java.lang.Object.wait(Object.java:503)
at liyin.code.App$1.run(App.java:11)
- locked <0x00000007aab0e868> (a java.lang.Class for liyin.code.App)
at java.lang.Thread.run(Thread.java:745)


分析:

haha
线程 和
hehe
线程分别从 Entry Set竞争到 App.class 对象的锁进入到 The Owner 区, 然后分别释放锁进入到 Wait Set队列.

haha
线程 和
hehe
线程分别调用 App.class.wait 释放锁, 行为是
in Object.wait()
,状态都变为
WAITING (on object monitor)
, 即进入无时限的等待状态.

WAITING
表示线程自己调用 Object.wait 方法进入无时限的等待状态, 这些线程自己不能够自行恢复, 必须等待另外一个线程进行唤醒。

结论
: wait和notify涉及到线程之间的协同, 如果dump文件中出现大量WAITING状态的线程, 需要结合源码从业务的角度分析确认这些线程是否有可能被唤醒、设计是否合理、是否实现协同上有问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 线程 dump jstack