Java 线程的状态及转换

大千世界 50 0

  低并发编程

  战略上藐视技术,战术上重视技术

  闪客:小宇你怎么了,我看你脸色很不好呀。

  小宇:今天去面试了,面试官问我 Java 线程的状态及其转化。

  闪客:哦哦,很常见的面试题呀,不是有一张状态流转图嘛。

  小宇:我知道,可是我每次面试的时候,脑子里记过的流转图就变成这样了。

  

Java 线程的状态及转换-第1张图片-大千世界


  闪客:哈哈哈。

  小宇:你还笑,气死我了,你能不能给我讲讲这些乱七八糟的状态呀。

  闪客:没问题,还是老规矩,你先把所有状态都忘掉,听我从头道来!

  小宇:好滴。

  线程状态的实质

  首先你得明白,当我们说一个线程的状态时,说的是什么?

  没错,就是一个变量的值而已。

  哪个变量?

  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(初始态)。

  

Java 线程的状态及转换-第2张图片-大千世界


  之后的分析,将弱化 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.cpp

  pthread_create();

  大名鼎鼎的 unix 创建线程的方法,pthread_create。

  此时,在操作系统内核中,才有了一个真正的线程,被创建出来。

  而 linux 操作系统,是没有所谓的刚创建但没启动的线程这种说法的,创建即刻开始运行。

  虽然无法从源码发现线程状态的变化,但通过 debug 的方式,我们看到调用了 Thread.start () 方法后,线程的状态变成了 RUNNABLE,运行态。

  那我们的状态图又丰富了起来。

  

Java 线程的状态及转换-第3张图片-大千世界


  线程 1 释放锁,线程 2、3、4 重新变为 RUNNABLE,继续抢锁,假如此时线程 3 抢到了锁。

  

Java 线程的状态及转换-第4张图片-大千世界


  如此往复。

  WAITING

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

  wait/notify

  我们在刚刚的 synchronized 块中加点东西。

  new Thread(() - {

  synchronized (lock) {

  ...

  lock.wait();

  ...

  }

  }).start();

  当这个 lock.wait () 方法一调用,会发生三件事。

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

  2. 线程状态变成 WAITING

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

  

Java 线程的状态及转换-第5张图片-大千世界


  什么时候这个线程被唤醒,从等待队列中移出,并从 WAITING 状态返回 RUNNABLE 状态呢?

  必须由另一个线程,调用同一个对象的 notify / notifyAll 方法。

  new Thread(() - {

  synchronized (lock) {

  ...

  lock.notify();

  ...

  }

  }).start();

  

Java 线程的状态及转换-第6张图片-大千世界


  只不过 notify 是只唤醒一个线程,而 notifyAll 是唤醒所有等待队列中的线程。

  但需要注意,被唤醒后的线程,从等待队列移出,状态变为 RUNNABLE,但仍然需要抢锁,抢锁成功了,才可以从 wait 方法返回,继续执行。

  如果失败了,就和上一部分的 BLOCKED 流程一样了。

  

Java 线程的状态及转换-第7张图片-大千世界


  所以我们的整个流程图,现在变成了这个样子。

  

Java 线程的状态及转换-第8张图片-大千世界


  join

  主线程这样写。

  public static void main(String[] args) {

  thread t = new Thread();

  t.start();

  t.join();

  }

  当执行到 t.join () 的时候,主线程会变成 WAITING 状态,直到线程 t 执行完毕,主线程才会变回 RUNNABLE 状态,继续往下执行。

  看起来,就像是主线程执行过程中,另一个线程插队加入(join),而且要等到其结束后主线程才继续。

  因此我们的状态图,又多了两项。

  

Java 线程的状态及转换-第9张图片-大千世界


  那 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.cpp

  void 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。现在,是不是更清晰了。

  

Java 线程的状态及转换-第10张图片-大千世界


  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 及操作系统层面的原理很复杂,改天可以专门找一节来讲解。

  现在我们的状态图,又可以更新了。

  

Java 线程的状态及转换-第11张图片-大千世界


  TIMED_WAITING

  这部分就再简单不过了,将上面导致线程变成 WAITING 状态的那些方法,都增加一个超时参数,就变成了将线程变成 TIMED_WAITING 状态的方法了,我们直接更新流程图。

  

Java 线程的状态及转换-第12张图片-大千世界


  这些方法的唯一区别就是,从 TIMED_WAITING 返回 RUNNABLE,不但可以通过之前的方式,还可以通过到了超时时间,返回 RUNNABLE 状态。

  就这样。

  还有,大家看。

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

  join 就是 wait 的封装。

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

  那有没有一个方法,仅仅让线程挂起,只能通过等待超时时间到了再被唤醒呢。

  这个方法就是

  Thread.sleep(long)

  我们把它补充在图里,这一部分就全了。

  

Java 线程的状态及转换-第13张图片-大千世界


  再把它加到全局图中。

  

Java 线程的状态及转换-第14张图片-大千世界


  后记

  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 调度执行的线程,处于死亡态。这样我的这门语言一定会有个经典面试题,为什么闪客把可运行的线程定义为死亡态呢?

  OK,今天的文章就到这里。

  本篇文章写得有点投入,写到这发现把开头都小宇都给忘了。


标签: Java

抱歉,评论功能暂时关闭!