`

Java 线程的状态及转换

    博客分类:
  • java
 
阅读更多

https://mp.weixin.qq.com/s/kjeWEXJfDz5pM3HLhYs9Xw

Java 线程的状态及转换

 
闪客:小宇你怎么了,我看你脸色很不好呀。小宇:今天去面试了,面试官问我 Java 线程的状态及其转化。闪客:哦哦,很常见的面试题呀,不是有一张状态流转图嘛。小宇:我知道,可是我每次面试的时候,脑子里记过的流转图就变成这样了。闪客:哈哈哈。小宇:你还笑,气死我了,你能不能给我讲讲这些乱七八糟的状态呀。闪客:没问题,还是老规矩,你先把所有状态都忘掉,听我从头道来!小宇:好滴。 

线程状态的实质

 首先你得明白,当我们说一个线程的状态时,说的是什么?没错,就是一个变量的值而已。哪个变量?Thread 类中的一个变量,叫private volatile int threadStatus = 0;这个值是个整数,不方便理解,可以通过映射关系(VM.toThreadState),转换成一个枚举类。

public enum State {

    NEW,

    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;

}

所以,我们就盯着 threadStatus 这个值的变化就好了。就是这么简单。 

NEW

 现在我们还没有任何 Thread 类的对象呢,也就不存在线程状态一说。一切的起点,要从把一个 Thread 类的对象创建出来,开始说起。Thread t = new Thread();当然,你后面可以接很多参数。Thread t = new Thread(r, "name1");你也可以 new 一个继承了 Thread 类的子类。Thread t = new MyThread();你说线程池怎么不 new 就可以有线程了呢?人家内部也是 new 出来的。
public class Executors {
  static class DefaultThreadFactory implements ThreadFactory {
  public Thread newThread(Runnable r) {
          Thread t = new Thread(...);
          ...
          return t;
      }
  }
}
总是,一切的开始,都要调用 Thread 类的构造方法。而这个构造方法,最终都会调用 Thread 类的 init() 方法。
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
  ...
  this.grout = g;
  this.name = name;
  ...
  tid = nextThreadID();
}
这个 init 方法,仅仅是给该 Thread 类的对象中的属性,附上值,除此之外啥也没干。它没有给 theadStatus 再次赋值,所以它的值仍然是其默认值。而这个值对应的状态,就是 STATE.NEW,非要翻译成中文,就叫初始态吧。因此说了这么多,其实就分析出了,新建一个 Thread 类的对象,就是创建了一个新的线程,此时这个线程的状态,是 NEW(初始态)图片之后的分析,将弱化 threadStatus 这个整数值了,就直接说改变了其线程状态,大家知道其实就只是改变了 threadStatus 的值而已。 

RUNNABLE

 你说,刚刚处于 NEW 状态的线程,对应操作系统里的什么状态呢?一看你就没仔细看我上面的分析。Thread t = new Thread();只是做了些表面功夫,在 Java 语言层面将自己的一个对象中的属性附上值罢了,根本没碰到操作系统级别的东西呢。所以这个 NEW 状态,不论往深了说还是往浅了说,还真就只是个无聊的枚举值而已。下面,精彩的故事才刚刚开始。躺在堆内存中无所事事的 Thread 对象,在调用了 start() 方法后,才显现生机。t.start();这个方法一调用,那可不得了,最终会调用到一个讨厌的 native 方法里。
private native void start0();
看来改变状态就并不是一句 threadStatus = xxx 这么简单了,而是有本地方法对其进行了修改。九曲十八弯跟进 jvm 源码之后,调用到了这个方法。
hotspot/src/os/linux/vm/os_linux.cpppthread_create(...);
大名鼎鼎的 unix 创建线程的方法,pthread_create。此时,在操作系统内核中,才有了一个真正的线程,被创建出来。而 linux 操作系统,是没有所谓的刚创建但没启动的线程这种说法的,创建即刻开始运行。虽然无法从源码发现线程状态的变化,但通过 debug 的方式,我们看到调用了 Thread.start() 方法后,线程的状态变成了 RUNNABLE,运行态。那我们的状态图又丰富了起来。图片通过这部分,我们知道如下几点:

1. 在 Java 调用 start() 后,操作系统中才真正出现了一个线程,并且立刻运行。

2. Java 中的线程,和操作系统内核中的线程,是一对一的关系。

3. 调用 start 后,线程状态变为 RUNNABLE,这是由 native 方法里的某部分代码造成的。

 

RUNNING 和 READY

 CPU 一个核心,同一时刻,只能运行一个线程。具体执行哪个线程,要看操作系统 的调度机制。所以,上面的 RUNNABLE 状态,准确说是,得到了可以随时准备运行的机会的状态。而处于这个状态中的线程,也分为了正在 CPU 中运行的线程,和一堆处于就绪中等待 CPU 分配时间片来运行的线程。图片处于就绪中的线程,会存储在一个就绪队列中,等待着被操作系统的调度机制选到,进入 CPU 中运行。当然,要注意,这里的 RUNNING 和 READY 状态,是我们自己为了方便描述而造出来的。无论是 Java 语言,还是操作系统,都不区分这两种状态,在 Java 中统统叫 RUNNABLE。 

TERMINATED

 当一个线程执行完毕(或者调用已经不建议的 stop 方法),线程的状态就变为 TERMINATED。图片此时这个线程已经无法死灰复燃了,如果你此时再强行执行 start 方法,将会报出错误。java.lang.IllegalThreadStateException很简单,因为 start 方法的第一行就是这么直戳了当地写的。
public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    ...
}
诶,那如果此时强行把 threadStatus 改成 0,会怎么样呢?你可以试试哟。 

BLOCKED

 上面把最常见,最简单的线程生命周期讲完了。初始 -- 运行 -- 终止没有发生任何的障碍。接下来,就稍稍复杂一点了,我们让线程碰到些障碍。首先创建一个对象 lock。
public static final Object lock = new Object();
一个线程,执行一个 sychronized 块,锁对象是 lock,且一直持有这把锁不放。
new Thread(() -> {
    synchronized (lock) {
        while(true) {}
    }
}).start();
另一个线程,也同样执行一个锁对象为 lock 的 sychronized 块。
new Thread(() -> {
    synchronized (lock) {
       ...
    }
}).start();
那么,在进入 synchronized 块时,因为无法拿到锁,会使线程状态变为 BLOCKED同样,对于 synchronized 方法,也是如此。当该线程获取到了锁后,便可以进入 synchronized 块,此时线程状态变为 RUNNABLE。因此我们得出如下转换关系。图片当然,这只是线程状态的改变,线程还发生了一些实质性的变化。我们不考虑虚拟机对 synchronized 的极致优化。当进入 synchronized 块或方法,获取不到锁时,线程会进入一个该锁对象的同步队列当持有锁的这个线程,释放了锁之后,会唤醒该锁对象同步队列中的所有线程,这些线程会继续尝试抢锁。如此往复。比如,有一个锁对象 A,线程 1 此时持有这把锁。线程 2、3、4 分别尝试抢这把锁失败。图片线程 1 释放锁,线程 2、3、4 重新变为 RUNNABLE,继续抢锁,假如此时线程 3 抢到了锁。图片如此往复。 

WAITING


这部分是最复杂的,同时也是面试中考点最多的,将分成三部分讲解。听我说完后你会发现,这三部分有很多相同但地方,不再是孤立的知识点。

wait/notify

我们在刚刚的 synchronized 块中加点东西。
new Thread(() -> {
    synchronized (lock) {
       ...
       lock.wait();
       ...
    }
}).start();
当这个 lock.wait() 方法一调用,会发生三件事。

1. 释放锁对象 lock(隐含着必须先获取到这个锁才行)

2. 线程状态变成 WAITING

3. 线程进入 lock 对象的等待队列

图片什么时候这个线程被唤醒,从等待队列中移出,并从 WAITING 状态返回 RUNNABLE 状态呢?必须由另一个线程,调用同一个对象的 notify/notifyAll 方法。
new Thread(() -> {
    synchronized (lock) {
       ...
       lock.notify(); 
       ...
    }
}).start();
图片只不过 notify 是只唤醒一个线程,而 notifyAll 是唤醒所有等待队列中的线程。但需要注意,被唤醒后的线程,从等待队列移出,状态变为 RUNNABLE,但仍然需要抢锁,抢锁成功了,才可以从 wait 方法返回,继续执行。如果失败了,就和上一部分的 BLOCKED 流程一样了。图片所以我们的整个流程图,现在变成了这个样子。图片

join

主线程这样写。
public static void main(String[] args) {
  thread t = new Thread(...);
  t.start();
  t.join();
  ...
}
当执行到 t.join() 的时候,主线程会变成 WAITING 状态,直到线程 t 执行完毕,主线程才会变回 RUNNABLE 状态,继续往下执行。看起来,就像是主线程执行过程中,另一个线程插队加入(join),而且要等到其结束后主线程才继续。因此我们的状态图,又多了两项。图片那 join 又是怎么神奇地实现这一切呢?也是像 wait 一样放到等待队列么?打开 Thread.join() 的源码,你会发现它非常简单。
// Thread.java
// 无参的 join 有用的信息就这些,省略了额外分支
public synchronized void join() {
  while (isAlive()) {
      wait();
  }
}
也就是说,他的本质仍然是执行了 wait() 方法,而锁对象就是 Thread t 对象本身。那从 RUNNABLE 到 WAITING,就和执行了 wait() 方法完全一样了。那从 WAITING 回到 RUNNABLE 是怎么实现的呢?主线程调用了 wait ,需要另一个线程 notify 才行,难道需要这个子线程 t 在结束之前,调用一下 t.notifyAll() 么?答案是否定的,那就只有一种可能,线程 t 结束后,由 jvm 自动调用 t.notifyAll(),不用我们程序显示写出。没错,就是这样。怎么证明这一点呢?道听途说可不行,老子今天非要扒开 jvm 的外套。果然,找到了如下代码。
hotspot/src/share/vm/runtime/thread.cppvoid JavaThread::exit(...) {  ...  ensure_join(this);  ...}static void ensure_join(JavaThread* thread) {  ...  lock.notify_all(thread);  ...}
我们看到,虚拟机在一个线程的方法执行完毕后,执行了个 ensure_join 方法,看名字就知道是专门为 join 而设计的。而继续跟进会发现一段关键代码,lock.notify_all,这便是一个线程结束后,会自动调用自己的 notifyAll 方法的证明。所以,其实 join 就是 wait,线程结束就是 notifyAll。现在,是不是更清晰了。图片

park/unpark

有了上面 wait 和 notify 的机制,下面就好理解了。一个线程调用如下方法。LockSupport.park()该线程状态会从 RUNNABLE 变成 WAITING、另一个线程调用LockSupport.unpark(Thread 刚刚的线程)刚刚的线程会从 WAITING 回到 RUNNABLE但从线程状态流转来看,与 wait 和 notify 相同。从实现机制上看,他们甚至更为简单。

1. park 和 unpark 无需事先获取锁,或者说跟锁压根无关。

2. 没有什么等待队列一说,unpark 会精准唤醒某一个确定的线程。

3. park 和 unpark 没有顺序要求,可以先调用 unpark

关于第三点,就涉及到 park 的原理了,这里我只简单说明。线程有一个计数器,初始值为0调用 park 就是

如果这个值为0,就将线程挂起,状态改为 WAITING。如果这个值为1,则将这个值改为0,其余的什么都不做。

调用 unpark 就是

将这个值改为1

然后我用三个例子,你就基本明白了。
// 例子1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以运行到这");
// 例子2
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以运行到这");
// 例子3
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
LockSupport.park(); // WAITING
System.out.println("不可以运行到这");
park 的使用非常简单,同时也是 JDK 中锁实现的底层。它的 JVM 及操作系统层面的原理很复杂,改天可以专门找一节来讲解。现在我们的状态图,又可以更新了。图片 TIMED_WAITING 这部分就再简单不过了,将上面导致线程变成 WAITING 状态的那些方法,都增加一个超时参数,就变成了将线程变成 TIMED_WAITING 状态的方法了,我们直接更新流程图。图片这些方法的唯一区别就是,从 TIMED_WAITING 返回 RUNNABLE,不但可以通过之前的方式,还可以通过到了超时时间,返回 RUNNABLE 状态。就这样。还有,大家看。

wait 需要先获取锁,再释放锁,然后等待被 notify。

join 就是 wait 的封装。

park 需要等待 unpark 来唤醒,或者提前被 unpark 发放了唤醒许可。

那有没有一个方法,仅仅让线程挂起,只能通过等待超时时间到了再被唤醒呢。这个方法就是Thread.sleep(long)我们把它补充在图里,这一部分就全了。图片再把它加到全局图中。图片

 

后记




 

Java 线程的状态,有六种

NEW

RUNNABLE

BLOCKED

WAITING

TIMED_WAITING

TERMINATED

而经典的线程五态模型,有五种状态

创建

就绪

执行

阻塞

终止

不同实现者,可能有合并和拆分。

比如 Java 将五态模型中的就绪和执行,都统一成 RUNNABLE,将阻塞(即不可能得到 CPU 运行机会的状态)细分为了 BLOCKED、WAITING、TIMED_WAITING,这里我们不去评价好坏。

也就是说,BLOCKED、WAITING、TIMED_WAITING 这几个状态,线程都不可能得到 CPU 的运行权,你叫它挂起、阻塞、睡眠、等待,都可以,很多文章,你也会看到这几个词没那么较真地来回用。

再说两个你可能困惑的问题。

调用 jdk 的 Lock 接口中的 lock,如果获取不到锁,线程将挂起,此时线程的状态是什么呢?

有多少同学觉得应该和 synchronized 获取不到锁的效果一样,是变成 BLOCKED 状态?不过如果你仔细看我上面的文章,有一句话提到了,jdk 中锁的实现,是基于 AQS 的,而 AQS 的底层,是用 park 和 unpark 来挂起和唤醒线程,所以应该是变为 WAITING 或 TIMED_WAITING 状态。调用阻塞 IO 方法,线程变成什么状态?比如 socket 编程时,调用如 accept(),read() 这种阻塞方法时,线程处于什么状态呢?答案是处于 RUNNABLE 状态,但实际上这个线程是得不到运行权的,因为在操作系统层面处于阻塞态,需要等到 IO 就绪,才能变为就绪态。但是在 Java 层面,JVM 认为等待 IO 与等待 CPU 执行权,都是一样的,人家就是这么认为的,这里我仍然不讨论其好坏,你觉得这么认为不爽,可以自己设计一门语言,那你想怎么认为,别人也拿你没办法。比如要我设计语言,我就认为可被 CPU 调度执行的线程,处于死亡态。这样我的这门语言一定会有个经典面试题,为什么闪客把可运行的线程定义为死亡态呢?

 

图片

 

 

分享到:
评论

相关推荐

    java线程状态转换图

    Java 线程状态转换图 Java 线程状态转换图是 Java 编程中非常重要的一个概念,它描述了线程在不同的状态之间的转换关系。了解线程状态转换图对 Java 编程的理解和应用非常重要。本文将详细介绍 Java 线程状态转换图...

    Java线程状态流转图

    Java线程状态流转图中,以下方法与线程状态转换相关: * Object.wait():使线程从RUNNABLE状态转换到WAITING状态。 * Object.notify():使线程从WAITING状态转换到RUNNABLE状态。 * Object.notifyAll():使多个线程...

    Java线程:线程状态的转换

    ### Java线程:线程状态的转换 #### 一、线程状态及其转换 ...通过以上分析,我们可以看到Java线程状态转换的基本原理以及如何利用线程状态来控制线程的行为,这对于开发高性能、高可靠性的并发程序至关重要。

    Java线程状态转换.pdf

    Java线程状态转换是Java多线程编程中的关键概念,对于理解和优化并发程序至关重要。Java线程在其生命周期中经历多种状态,这些状态之间的转换是由线程调度器根据特定的策略来决定的。以下是对Java线程状态转换的详细...

    Java线程:线程状态的转换.pdf

    在Java编程中,线程是并发执行任务的...总之,理解和掌握Java线程状态的转换以及如何控制线程执行是编写高效并发程序的关键。通过适当的方法,我们可以控制线程的执行顺序,实现线程间的同步和协作,从而优化程序性能。

    Java学习教程-探究JAVA线程状态及转化视频

    Java线程是并发编程的核心部分,...总的来说,Java线程状态和转换是Java并发编程的基础,对于提升软件的并发性和响应性具有重要意义。通过本教程的视频学习,你将能更深入地理解这些概念,并能够在实际开发中灵活运用。

    Java线程:线程状态的转换[参考].pdf

    线程状态转换是一个复杂的过程,涉及到线程调度、锁的管理等多个方面。了解这些状态和转换机制,有助于编写高效、可控的多线程程序。在实际编程中,应根据需求合理利用这些状态控制,确保线程间的协作和资源利用达到...

    Java线程状态转换关系实例解析

    Java线程状态转换关系实例解析 Java线程状态转换关系实例解析是Java多线程编程中的一种重要概念,它指的是Java线程在不同的状态之间的转换关系。了解这些状态转换关系可以帮助开发者更好地编写多线程程序,避免线程...

    Java-多线程线程状态转换图

    多线程线程状态转换图

    Java线程.ppt

    学习Java线程,理解其创建、状态转换、调度和控制,以及如何处理线程间的互斥和同步,对于开发高效、稳定的并发程序至关重要。通过深入研究这些概念,开发者能够编写出更符合现代计算需求的高质量软件。

    Java源码查看线程的运行状态.rar

    在Java编程中,线程是程序执行的基本单元,它允许应用程序同时执行多个任务。了解如何查看线程的运行状态对于调试和性能优化...通过阅读和分析源码,我们可以更深入地了解Java线程的工作原理,从而提升我们的编程技能。

    Java多线程编程总结

    Java线程:线程状态的转换 Java线程:线程的同步与锁 Java线程:线程的交互 Java线程:线程的调度-休眠 Java线程:线程的调度-优先级 Java线程:线程的调度-让步 Java线程:线程的调度-合并 Java线程:线程的调度-...

    关于线程(java)两天的课件

    Java线程有五种基本状态:新建(New)、可运行(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Terminated)。这些状态之间的转换是通过调用特定的方法,如start()、sleep()、join()、wait()、notify()等...

    java线程详解

    Java线程:线程状态的转换 Java线程:线程的同步与锁 一、同步问题提出 二、同步和锁定 三、静态方法同步 四、如果线程不能不能获得锁会怎么样 五、何时需要同步 六、线程安全类 七、线程死锁 八、线程同步...

    电子书《java线程》

    线程的生命周期包括新建、就绪、运行、阻塞和终止等状态,理解这些状态以及如何在这些状态之间转换是理解和使用Java线程的基础。 书中可能涵盖了以下几个关键知识点: 1. **线程的创建与启动**:通过创建Thread...

    Java线程详解.ppt

    Java线程是并发编程的核心部分,它允许程序在同一时间执行多个任务,从而提高了系统的效率和资源利用率。在Java中,线程是通过`Thread`类或者实现`Runnable`接口来创建和管理的。 首先,我们要理解进程和线程的概念...

    Java多线程编程经验

    #### 五、Java线程:线程状态的转换 线程的状态主要有:新状态(New)、可运行状态(Runnable)、阻塞状态(Blocked)、等待状态(Waiting)、定时等待状态(Timed Waiting)以及终止状态(Terminated)。这些状态...

    java线程有关例题

    11.4章节“线程状态的改变”解释了如何通过`sleep()`, `join()`, `wait()`, `notify()`, `notifyAll()`等方法来控制线程状态的转换。例如,`sleep()`会让当前线程进入定时等待状态,`join()`可以让一个线程等待另一...

    java多线程编程总结

    #### 四、Java线程:线程状态的转换 - **线程的状态** Java线程的状态包括新建 (`NEW`)、就绪 (`RUNNABLE`)、阻塞 (`BLOCKED`)、等待 (`WAITING`)、超时等待 (`TIMED_WAITING`) 和终止 (`TERMINATED`)。 #### 五...

    Java线程状态及切换、关闭线程的正确姿势分享

    "Java线程状态及切换、关闭线程的正确姿势分享" 本文将详细介绍Java线程状态及切换、关闭线程的相关内容。线程状态及切换是Java多线程编程中一个非常重要的概念,掌握线程状态及切换是编写高效、稳定、可靠的多线程...

Global site tag (gtag.js) - Google Analytics