只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

51、线程状态

内容来自王争 Java 编程之美

前面我们重点讲解的是如何编写运行在多线程下的代码而非线程本身,从本节开始我们重点讲解线程本身相关的内容,包括:线程状态、线程池、创建执行、线程中断等

在项目开发中,当代码运行出现问题时,比如死循环、死锁等,我们一般会通过 jstack 工具打印线程运行信息,以此来分析问题出现的原因
线程状态是其中非常重要的一项信息,透彻的理解线程状态,比如熟悉 Thread.sleep()、Object.wait()、Lock.lock() 等一些常用函数与线程状态的对应关系
才能完全看懂 jstack 打印的线程运行信息,以便快速找出问题的原因,因此本节我们就来详细讲一讲线程状态

在开始本节的内容前,我还是照例给你出一个思考题
线程在执行 synchronized 阻塞等待锁时,对应的线程状态为 BLOCKED,而线程在执行 Lock 锁的 lock() 函数阻塞等待锁时,对应的线程状态为 WAITING
同样是阻塞等待锁,为什么对应的线程状态却是不同的呢?

带着这个问题,我们来开始本节的学习

1、线程状态概述

在线程模块的一开始,我们讲解了线程的一些基础知识,当时讲到 Java 线程是基于 1:1 模型实现的,核心调度执行是基于操作系统线程实现的

1.1、操作系统线程状态

操作系统线程在运行的过程中,不同情况对应不同的线程状态,不同的操作系统定义的线程状态有可能不同,我们拿 Linux 举例,其定义的线程状态有如下几个

  • NEW:新创建的线程,在没有调用 start() 函数前,线程处于 NEW 状态
  • READY:线程一切就绪,等待操作系统调度,也就是等待 CPU 时间片
  • RUNNING:线程正在使用 CPU 时间片执行程序
  • WAITING:线程在等待 I / O 读写完成、等待获取锁、等待时钟定时到期(调用 sleep() 函数)等等
    总之等待其他事件发生之后,线程才能被调度使用 CPU,此时,线程的状态就是 WAITING 状态
    也就是说,只要线程不占用 CPU,并且不等待 CPU(非 READY),那么就处于 WAITING 状态
  • TERMINATED:线程终止状态,线程终止之后,未必就立即销毁
    有些操作系统为了节省线程创建的时间(毕竟要分配内存还得初始化一些变量),会复用处于 TERMINATED 状态的线程

上述操作系统线程状态之间的转换关系,如下图所示
image

1.2、Java 线程状态

尽管 Java 线程是基于操作系统线程实现的,但是 Java 线程没有直接使用操作系统定义的线程状态

  • 一方面是因为:Java 是跨平台的,不同操作系统定义的线程状态不同,无法统一
  • 另一方面是因为:应用层关注的线程状态,跟操作系统关注的线程状态是不同的,Java 线程定义的状态更能清晰表示程序在应用层的执行情况

Java 定义的线程状态如下所示

  • NEW:Java 线程中 NEW 状态的含义,跟操作系统线程中 NEW 状态的含义相同
  • RUNNABLE:在应用层,我们是不需要关注程序是正在等待 CPU 时间片,还是正在使用 CPU 时间这些操作系统才需要关注的细节,我们只需要知道程序正在执行就可以了
    因此在 Java 的线程状态定义中,并没有 READY 和 RUNNING 的区分,两者统称为 RUNNABLE
    实际上 RUNNABLE 除了包含 READY 和 RUNNING 之外,还包含操作系统线程状态中的部分 WAITING 状态,待会再专门解释这一点
  • WAITING:这里的 WAITING 状态跟操作系统线程中的 WAITING 状态不同
    只有执行一些跟线程有关的特殊函数时,线程才会进入 WAITING 状态,这些特殊函数就包含:Object.wait()、Thread.join()、Unsafe.park()
  • TIMED_WAITING:跟上面的 WAITING 状态类似,也是只有执行一些跟线程有关的特殊函数时,线程才会进入 TIMED_WAITING 状态
    这些特殊函数就包括:Object.wait(long timeout)、Thread.join(long timeout)、Unsafe.parkNanos(long timeout)、Thread.sleep(long timeout),这些函数均带有超时时间
  • BLOCKED:线程进入 BLOCKED 状态,只对应两种情况
    一种情况是线程执行 synchronized 语句,阻塞等待获取锁,另一种情况是线程执行 Object.wait() 后被notify() 或 notifyAll() 唤醒,再次阻塞等待获取锁
  • TERMINATED:Java 线程中 TERMINATED 状态的含义,跟操作系统线程中 TERMINATED 状态的含义相同

上述 Java 线程状态之间的转换关系,如下图所示

  • BLOCKED:等待锁(仅 synchronized,Lock 锁不在这里)
  • WAITING、TIMED_WAITING:等待资源(Lock 锁在这里)

image

1.3、Java 线程状态解释

对于 Java 线程状态,我们做两点解释

第一点解释:前面提到 Java 线程中的 RUNNABLE 状态包含操作系统线程中的 READY 状态、RUNNING 状态、部分 WAITING 状态

当线程执行阻塞 I / O 函数(比如等待磁盘或网络 I / O 读写就绪)时,在操作系统层面,因为 CPU 没有在执行,所以操作系统将对应的线程状态设置为 WAITING
但在 Java 应用层面,因为其不关心底层硬件的使用情况(CPU 有没有在执行),所以 Java 将对应的线程状态设置为 RUNNABLE
也就是说 Java 线程状态定义和操作系统线程状态定义是不一致的,它们的对应关系如下图所示
image

第二点解释:线程执行 synchronized 时,对应的线程状态为 BLOCKED,那么线程执行 JUC Lock 的 lock() 函数时,对应的线程状态是什么呢?

因为 JUC Lock 上的 lock() 函数底层调用 LockSupport.park() 进行阻塞,而 LockSupport.park() 底层又调用了 Unsafe.park()
前面讲到,当线程执行 Unsafe.park() 时,对应的线程状态为 WAITING,因此线程执行 JUC Lock 上的 lock() 函数时,对应线程状态是 WAITING
那么为什么两种加锁方式对应的线程状态不同呢?

实际上 Java 线程状态的修改是在 JVM 层面实现的,并且没有提供专门的修改线程状态的函数给上层(JDK 和 JUC)使用
而是将线程状态的修改耦合在了一些 JVM 提供的特殊函数和语法中(比如 Thread.sleep()、Object.wait()、synchronized)
JUC 在实现 Lock 时,因为用到了 JVM 提供的 Unsafe.park(),从而导致线程状态为 WAITING
image

你可能会说 synchronized 不也用到了 park() 函数吗?为什么它对应的线程状态就是 BLOCKED,而非 WAITING 呢?
实际上到目前为止,我们讲到了 3 种不同的 park() 函数:synchronized 使用的 park() 函数、Unsafe.park()、LockSupport.park()
这 3 个 park() 函数的功能相同,代码实现也大同小异,但是基于分层的设计思路,它们的使用场景是不同的

  • synchronized 使用的 park() 函数是 JVM 内部私有的函数,只供实现 synchronized 使用
  • Unsafe.park() 是开放给 Java 开发者使用的函数
  • LockSupport.park() 函数是 JUC 为了方便使用,对 Unsafe.park() 的简单二次封装

JVM 在实现 synchronized 时,将线程状态设置为 BLOCKED,在实现 Object.wait() 时,将线程状态设置为 WAITING,这些设置都非常合理、并没有争议
但是当实现 Unsafe.park() 时,到底将线程状态设置为 BLOCKED 还是 WAITING,就有争议,于是 JVM 就任选了其中一个
这也就导致线程在执行 JUC Lock 的 lock() 函数时,对应的线程状态为 WAITING

你可能会说,如果当初 JVM 将 Unsafe.park() 对应的线程状态设置为 BLOCKED,那么 synchronized 和 Lock 两种不同加锁方式对应线程状态不同这个问题就不存在了
实际上尽管这个问题不存在了,但也会有新的问题产生
我们知道 JUC 中的 Condition 在实现时,也用到了 Unsafe.park()
这就导致线程在执行 Condition 上的 await() 函数时,对应的线程状态变为了 BLOCKED,于是这就跟 Object.wait() 的线程状态又不一致了

2、线程状态与函数对应关系

jstack 是 JVM 自带的一个线程信息打印工具,能够打印线程的快照信息,比如:函数调用栈信息、线程状态等
jstack 常用于定位线程长时间卡顿问题,比如:死锁、死循环等
利用这个工具,我们可以清晰地知道:线程执行到哪里、处于什么状态、等待什么资源等

jstack 的使用非常简单,先通过另一个命令 jps,列出所有的 Java 进程 ID,查找要打印线程信息的进程 ID,然后使用 jstack [进程 ID] 命令即可打印进程包含的所有线程的信息
接下来我们结合示例代码并使用 jstack,查看一下常用的一些函数对应的线程状态(Java JPS 的全称是 Java Virtual Machine Process Status Tool)

2.1、示例代码一

示例代码一展示 synchronized、Thread.sleep(long time)、Thread.join() 对应的线程状态

public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Demo.class) {
try {
Thread.sleep(1000000); // 1000 s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Demo.class) {
}
}
}, "t2");
t2.start();
t1.join();
t2.join();
}
}

jstack 的打印结果如下图

  • 线程 t1 加锁成功之后,执行 sleep() 函数,对应的线程状态为 TIMED_WAITING
  • 线程 t2 执行 synchronized,阻塞等待获取锁,对应的线程状态为 BLOCKED
  • 线程 main 执行 join() 等待 t1、t2 执行结束,对应的线程状态为 WAITING

这里稍微提一句,因为 join() 函数是基于 Object.wait() 实现的,因此 main 线程的函数调用栈的最顶层为 Object.wait() 函数
image

2.2、示例代码二

示例代码二展示 Object.wait() 对应的线程状态

public class Demo {
private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");
t1.start();
t1.join();
}
}

jstack 的打印结果如下图,线程 t1 执行 Object.wait() 函数,对应的线程状态为 WAITING
image

前面讲到,当 Object.wait() 被 notify() 或 notifyAll() 唤醒后,会再次阻塞等待获取锁,此时线程处于 BLOCKED 状态,我们举个例子验证一下

public class Demo {
private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
try {
obj.wait();
Thread.sleep(100000); // 100 s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t2");
t2.start();
synchronized (obj) {
obj.notifyAll();
}
t1.join();
t2.join();
}
}

jstack 的打印结果如下图

  • 线程 t1 和 t2 均调用 wait() 函数,导致 t1 和 t2 阻塞
  • 线程 main 执行 notifyAll() 函数,唤醒线程 t1 和 t2
    两个线程同时竞争锁,t1 获取到锁,执行 sleep(),对应线程状态为 TIMED_WAITING
  • 线程 t2 等待线程 t1 释放锁,对应线程状态为 BLOCKED

image

2.3、示例代码三

示例代码三展示 Lock.lock() 对应的线程状态

public class Demo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(100000); // 100 s
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("do nothing...");
lock.unlock();
}
}, "t2");
t2.start();
t1.join();
t2.join();
}
}

jstack 的打印结果如下图

  • 线程 t1 获取到 Lock 锁之后,执行 sleep() 函数,因此对应的线程状态是 TIMED_WAITING
  • 线程 t2 等待获取 Lock 锁,底层调用 LockSupport.park() 函数,而 LockSupport.park() 函数又调用 Unsafe.park() 函数,因此对应的线程状态为 WAITING

image

2.4、示例代码四

示例代码四展示 Condition.await()、Semahpore.aquire() 对应的线程状态

public class Demo {
private static final Lock lock = new ReentrantLock();
private static final Semaphore sem = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
sem.acquireUninterruptibly(2);
lock.newCondition().awaitUninterruptibly();
lock.unlock();
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
sem.acquireUninterruptibly();
}
}, "t2");
t2.start();
t1.join();
t2.join();
}
}

jstack 的打印结果如下图

  • 线程 t1 阻塞在 Condition.awaitUninterruptibly() 函数上
  • 线程 t2 阻塞在 Semaphore.acquireInterruptibly() 函数上
  • 这两个函数都是基于 LockSupport.park() 实现阻塞功能,而 LockSupport.park() 函数又调用 Unsafe.park() 函数,因此对应的线程状态都为 WAITING

image

3、线程状态与 jstack 的应用

在平时的开发中,我们有时候会遇到 CPU 占用率 100%、请求超时等问题,当出现以上这些问题时,我们可以通过 jstack 查看线程运行的详细信息来定义代码问题

CPU 占用率 100% 往往是因为持续执行 CPU 高度密集操作,比如:进入死循环、垃圾回收等
因为在大部分业务系统中,业务的处理往往会涉及磁盘、网络等 I / O 操作,这些非 CPU 操作和 CPU 操作交替执行,很难将 CPU 占用率拉满
只有当程序长时间集中执行 CPU 操作时,CPU 占用率才有可能被拉满
这时我们就可以使用 jstack,每隔几秒将所有的线程运行信息都打印出来,然后综合相邻的这几次 jstack 结果,定位哪个线程都在执行长时间集中执行 CPU 密集操作

请求超时往往是因为:死锁、下游请求阻塞、下游请求超时长,我们重点关注其中的死锁
当我们通过 jstack 打印出进程的所有线程信息时,如果两个线程互相等待对方持有的锁,那么就说明出现了死锁,示例代码如下所示

public class Demo {
private static final Object obj1 = new Object();
private static final Object obj2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj1) {
try {
Thread.sleep(1000); // 1 s
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
}
}
}
}, "t1");
t1.start();
Thread.sleep(500);
synchronized (obj2) {
synchronized (obj1) {
}
}
}
}

jstack 打印的结果如下所示,jstack 非常人性化地帮我们查找并列出了死锁
image

4、课后思考题

我们可以在资源管理器中查看 CPU 的利用率动态曲线,请编写程序让 CPU 利用率动态曲线呈现 sin 函数曲线
线程运行结束之后直接销毁即可,为什么还要定义 TERMINATED 这种线程状态?
线程终止之后未必就立即销毁,操作系统为了节省线程创建的时间(毕竟要分配内存还得初始化一些变量),会复用处于 TERMINATED 状态的线程

posted @   lidongdongdong~  阅读(34)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开