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

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 lockwaiting 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之间关系,以及线程的状态转换图:

线程和Monitor之间关系

  • 进入区(Entrt Set) : 表示线程通过synchronized要求获取对象的锁。如果对象未被锁住, 则进入拥有者; 否则则在进入区等待。一旦对象锁被其他线程释放, 立即参与竞争。
  • 拥有者(The Owner) : 表示某一线程成功竞争到对象锁。
  • 等待区(Wait Set) : 表示线程通过对象的wait方法, 释放对象的锁, 并在等待区等待被唤醒。

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

四、线程状态实例解析

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

状态为NEW的线程未启动, 不会出现在dump文件中, RUNNABLETERMINATED 容易理解就不讨论了, 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状态的线程, 需要结合源码从业务的角度分析确认这些线程是否有可能被唤醒、设计是否合理、是否实现协同上有问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值