09 | Java线程(上):Java线程的生命周期

在Java领域,实现并发程序的主要手段就是多线程。线程是操作系统里的一个概念,虽然各种不同的开发语言如Java、C#等都对其进行了封装,但是万变不离操作系统。Java语言里的线程本质上就是操作系统的线程,它们是一一对应的。

在操作系统层面,线程也有“生老病死”,专业的说法叫有生命周期。对于有生命周期的事物,要学好它,思路非常简单,只要能搞懂生命周期中各个节点的状态转换机制就可以了。

虽然不同的开发语言对于操作系统线程进行了不同的封装,但是对于线程的生命周期这部分,基本上是雷同的。所以,我们可以先来了解一下通用的线程生命周期模型,这部分内容也适用于很多其他编程语言;然后再详细有针对性地学习一下Java中线程的生命周期。

通用的线程生命周期

通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态终止状态

通用线程状态转换图——五态模型

这“五态模型”的详细情况如下所示。

  1. 初始状态,指的是线程已经被创建,但是还不允许分配CPU执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
  2. 可运行状态,指的是线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配CPU执行。
  3. 当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态
  4. 运行状态的线程如果调用一个阻塞的API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
  5. 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

这五种状态在不同编程语言里会有简化合并。例如,C语言的POSIX Threads规范,就把初始状态和可运行状态合并了;Java语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而JVM层面不关心这两个状态,因为JVM把线程调度交给操作系统处理了。

除了简化合并,这五种状态也有可能被细化,比如,Java语言里就细化了休眠状态(这个下面我们会详细讲解)。

Java中线程的生命周期

介绍完通用的线程生命周期模型,想必你已经对线程的“生老病死”有了一个大致的了解。那接下来我们就来详细看看Java语言里的线程生命周期是什么样的。

Java语言中线程共有六种状态,分别是:

  1. NEW(初始化状态)
  2. RUNNABLE(可运行/运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

这看上去挺复杂的,状态类型也比较多。但其实在操作系统层面,Java线程中的BLOCKED、WAITING、TIMED_WAITING是一种状态,即前面我们提到的休眠状态。也就是说只要Java线程处于这三种状态之一,那么这个线程就永远没有CPU的使用权

所以Java线程的生命周期可以简化为下图:

Java中的线程状态转换图

其中,BLOCKED、WAITING、TIMED_WAITING可以理解为线程导致休眠状态的三种原因。那具体是哪些情形会导致线程从RUNNABLE状态转换到这三种状态呢?而这三种状态又是何时转换回RUNNABLE的呢?以及NEW、TERMINATED和RUNNABLE状态是如何转换的?

1. RUNNABLE与BLOCKED的状态转换

只有一种场景会触发这种转换,就是线程等待synchronized的隐式锁。synchronized修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从RUNNABLE转换到BLOCKED状态。而当等待的线程获得synchronized隐式锁时,就又会从BLOCKED转换到RUNNABLE状态。

如果你熟悉操作系统线程的生命周期的话,可能会有个疑问:线程调用阻塞式API时,是否会转换到BLOCKED状态呢?在操作系统层面,线程是会转换到休眠状态的,但是在JVM层面,Java线程的状态不会发生变化,也就是说Java线程的状态会依然保持RUNNABLE状态。JVM层面并不关心操作系统调度相关的状态,因为在JVM看来,等待CPU使用权(操作系统层面此时处于可执行状态)与等待I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了RUNNABLE状态。

而我们平时所谓的Java在调用阻塞式API时,线程会阻塞,指的是操作系统线程的状态,并不是Java线程的状态。

2. RUNNABLE与WAITING的状态转换

总体来说,有三种场景会触发这种转换。

第一种场景,获得synchronized隐式锁的线程,调用无参数的Object.wait()方法。其中,wait()方法我们在上一篇讲解管程的时候已经深入介绍过了,这里就不再赘述。

第二种场景,调用无参数的Thread.join()方法。其中的join()是一种线程同步方法,例如有一个线程对象thread A,当调用A.join()的时候,执行这条语句的线程会等待thread A执行完,而等待中的这个线程,其状态会从RUNNABLE转换到WAITING。当线程thread A执行完,原来等待它的线程又会从WAITING状态转换到RUNNABLE。

第三种场景,调用LockSupport.park()方法。其中的LockSupport对象,也许你有点陌生,其实Java并发包中的锁,都是基于它实现的。调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从RUNNABLE转换到WAITING。调用LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从WAITING状态转换到RUNNABLE。

3. RUNNABLE与TIMED_WAITING的状态转换

有五种场景会触发这种转换:

  1. 调用带超时参数的Thread.sleep(long millis)方法;
  2. 获得synchronized隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法;
  3. 调用带超时参数的Thread.join(long millis)方法;
  4. 调用带超时参数的LockSupport.parkNanos(Object blocker, long deadline)方法;
  5. 调用带超时参数的LockSupport.parkUntil(long deadline)方法。

这里你会发现TIMED_WAITING和WAITING状态的区别,仅仅是触发条件多了超时参数

4. 从NEW到RUNNABLE状态

Java刚创建出来的Thread对象就是NEW状态,而创建Thread对象主要有两种方法。一种是继承Thread对象,重写run()方法。示例代码如下:

// 自定义线程对象
class MyThread extends Thread {
  public void run() {
    // 线程需要执行的代码
    ......
  }
}
// 创建线程对象
MyThread myThread = new MyThread();

另一种是实现Runnable接口,重写run()方法,并将该实现类作为创建Thread对象的参数。示例代码如下:

// 实现Runnable接口
class Runner implements Runnable {
  @Override
  public void run() {
    // 线程需要执行的代码
    ......
  }
}
// 创建线程对象
Thread thread = new Thread(new Runner());

NEW状态的线程,不会被操作系统调度,因此不会执行。Java线程要执行,就必须转换到RUNNABLE状态。从NEW状态转换到RUNNABLE状态很简单,只要调用线程对象的start()方法就可以了,示例代码如下:

MyThread myThread = new MyThread();
// 从NEW状态转换到RUNNABLE状态
myThread.start();

5. 从RUNNABLE到TERMINATED状态

线程执行完 run() 方法后,会自动转换到TERMINATED状态,当然如果执行run()方法的时候异常抛出,也会导致线程终止。有时候我们需要强制中断run()方法的执行,例如 run()方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java的Thread类里面倒是有个stop()方法,不过已经标记为@Deprecated,所以不建议使用了。正确的姿势其实是调用interrupt()方法。

那stop()和interrupt()方法的主要区别是什么呢?

stop()方法会真的杀死线程,不给线程喘息的机会,如果线程持有ReentrantLock锁,被stop()的线程并不会自动调用ReentrantLock的unlock()去释放锁,那其他线程就再也没机会获得ReentrantLock锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有suspend() 和 resume()方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。

而interrupt()方法就温柔多了,interrupt()方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被interrupt的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。

当线程A处于WAITING、TIMED_WAITING状态时,如果其他线程调用线程A的interrupt()方法,会使线程A返回到RUNNABLE状态,同时线程A的代码会触发InterruptedException异常。上面我们提到转换到WAITING、TIMED_WAITING状态的触发条件,都是调用了类似wait()、join()、sleep()这样的方法,我们看这些方法的签名,发现都会throws InterruptedException这个异常。这个异常的触发条件就是:其他线程调用了该线程的interrupt()方法。

当线程A处于RUNNABLE状态时,并且阻塞在java.nio.channels.InterruptibleChannel上时,如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptException这个异常;而阻塞在java.nio.channels.Selector上时,如果其他线程调用线程A的interrupt()方法,线程A的java.nio.channels.Selector会立即返回。

上面这两种情况属于被中断的线程通过异常的方式获得了通知。还有一种是主动检测,如果线程处于RUNNABLE状态,并且没有阻塞在某个I/O操作上,例如中断计算圆周率的线程A,这时就得依赖线程A主动检测中断状态了。如果其他线程调用线程A的interrupt()方法,那么线程A可以通过isInterrupted()方法,检测是不是自己被中断了。

总结

理解Java线程的各种状态以及生命周期对于诊断多线程Bug非常有帮助,多线程程序很难调试,出了Bug基本上都是靠日志,靠线程dump来跟踪问题,分析线程dump的一个基本功就是分析线程状态,大部分的死锁、饥饿、活锁问题都需要跟踪分析线程的状态。同时,本文介绍的线程生命周期具备很强的通用性,对于学习其他语言的多线程编程也有很大的帮助。

你可以通过 jstack 命令或者Java VisualVM这个可视化工具将JVM所有的线程栈信息导出来,完整的线程栈信息不仅包括线程的当前状态、调用栈,还包括了锁的信息。例如,我曾经写过一个死锁的程序,导出的线程栈明确告诉我发生了死锁,并且将死锁线程的调用栈信息清晰地显示出来了(如下图)。导出线程栈,分析线程状态是诊断并发问题的一个重要工具。

发生死锁的线程栈

课后思考

下面代码的本意是当前线程被中断之后,退出while(true),你觉得这段代码是否正确呢?

Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }
  // 省略业务代码无数
  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    e.printStackTrace();
  }
}

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

精选留言

  • 姜戈

    2019-03-19 01:06:39

    可能出现无限循环,线程在sleep期间被打断了,抛出一个InterruptedException异常,try catch捕捉此异常,应该重置一下中断标示,因为抛出异常后,中断标示会自动清除掉!
    Thread th = Thread.currentThread();
    while(true) {
    if(th.isInterrupted()) {
    break;
    }
    // 省略业务代码无数
    try {
    Thread.sleep(100);
    }catch (InterruptedException e){
    Thread.currentThread().interrupt();
    e.printStackTrace();
    }
    }
    作者回复

    👍👍👍

    2019-03-19 07:54:42

  • 虎虎❤️

    2019-03-19 12:33:32

    我的一位长辈曾告诉我,没有真正学不会的知识或者技术,只是缺乏好的老师。

    有的人可以把复杂的知识讲明白,但是讲解的过程却也是晦涩难懂,不免落了下成。

    而学习王老师的课,我一直都觉得很轻松。云淡风轻地就把并发知识抽丝剥茧,确是更显功力。另一方面,我觉得人的大脑更喜欢接受这些平易近人的文字。看似浅近的文字,却更能带领我深入的思考,留下更深刻的印象。反观一些看起来高端大气上档次的论述,让人觉得云山雾罩,好不容易看懂了,但看过后却什么也想不起来了。大概是读文章的时候脑细胞都用来和晦涩的文字做斗争了,已经没有空间去思考和记忆了。

    再次感谢王老师给大家带来优秀的课程。
    作者回复

    看来我没必要写的很装了😂

    2019-03-19 20:11:21

  • Geek_961eed

    2019-03-19 10:41:32

    希望作者讲解一下每一期的思考题!
  • Tristan

    2019-04-14 10:31:46

    为什么实战高并发程序设计医术中写道“Tread.stop()方法在结束线程时,会直接终止线程,并且会释放这个线程所持有的锁”,而您文中所写的“果线程持有 synchronized 隐式锁,也不会释放”??
    作者回复

    是我的错,我确认了一下,隐式锁可以释放。多谢多谢!!!

    2019-04-14 17:18:48

  • rock

    2019-06-20 19:55:21

    new, runnable, block, waiting, time_waiting, terminated

    runnable-->block, 仅线程等待synchronized隐式锁
    block-->runnable, 线程获得了synchronized隐式锁

    runnable-->waiting, 线程获得了synchronized隐式锁,在临界区类调用wait(),(无时间限制的)等待指定的条件满足。
    线程调用子线程的Thread.jion(),(无时间限制的)等待子线程执行完成返回。
    线程调用LockSupport.park(),(无时间限制的)等待LockSupport.unPark(thead)。【LockSupport.park(),用于创建锁和其他同步类的底层基本线程阻塞原语,使当前线程block from thread scheduling】
    waiting-->runnable, 上述3种,条件达成后。

    runnable-->time_waiting, 线程获得了synchronized隐式锁,在临界区类调用wait(time),(有时间限制的)等待指定的条件满足。
    线程调用子线程的Thread.jion(time),(有时间限制的)等待子线程执行完成返回。
    线程调用LockSupport.parkNanos(Object blocker, long deadline).
    线程调用LockSupport.partUntil(long deadline).
    线程调用Thread.sleep(long millis).
    time_waiting-->runnable, 上述条件满足 或 等待超时

    runnable-->teminated, 线程自动终结:1,线程run方法执行结束退出; 2,线程run方法内部抛出异常退出
    其他线程通过调用本线程的Tread.interrupt()方法,尝试强制中止本线程:【注:不要用Tread.stop()这个废弃方法,危害...】
    1) 本线程处于waiting,timed_waiting状态时,其他线程调用本线程的interrupt(),会使本线程转为Runnable状态,同时本线程抛出InterruptedException.
    2) 本线程处于Runnable状态,且阻塞在java.nio.channels.InterruptedChannel [可中断channel]时,其他线程调用本线程的interrupt(),会使本线程抛出ClosedByInterruptedException。
    3) 本线程处于Runnable状态,且阻塞在java.nio.channels.Selector[可多路复用的异步IO机制]上时,本线程会(从Selector.select())立即返回。
    4) 本线程处于Runnable状态,且没有阻塞在某个I/O操作上时,其他线程调用本线程的interrupt(),这只是将本线程的中断标志位置为True。
    本线程可以通过调用isInterrupted()来查看本线程的中断标志位是否被置为true,可以决定退出,也可以忽略它(全看代码逻辑)。

    重要:本线程抛出InterruptedException后,会把本线程的中断标志位清空,可能已有的中断标志True就消失了,可能会引起本线程失去主动监测中断标志以退出的机会!
    所以,对本线程抛出的InterruptedException的异常try-catch后,再主动置标志位为True。Thead.currentTread().interrupt();
  • thas

    2019-03-19 15:40:18

    interrupt是中断的意思,在单片机开发领域,用于接收特定的事件,从而执行后续的操作。Java线程中,(通常)使用interrupt作为线程退出的通知事件,告知线程可以结束了。
    interrupt不会结束线程的运行,在抛出InterruptedException后会清除中断标志(代表可以接收下一个中断信号了),所以我想,interrupt应该也是可以类似单片机一样作为一种通知信号的,只是实现通知的话,Java有其他更好的选择。
    因InterruptedException退出同步代码块会释放当前线程持有的锁,所以相比外部强制stop是安全的(已手动测试)。sleep、join等会抛出InterruptedException的操作会立即抛出异常,wait在被唤醒之后才会抛出异常(就像阻塞一样,不被打扰)。
    另外,感谢老师提醒,I/O阻塞在Java中是可运行状态,并发包中的lock是等待状态。
    作者回复

    能和硬件中断联系起来👍👍👍

    2019-03-19 20:04:43

  • Junzi

    2019-03-26 10:08:10

    当发起中断之后,Thread.sleep(100);会抛出InterruptedException异常,而这个抛出这个异常会清除当前线程的中断标识,导致th.isInterrupted()一直都是返回false的。

    InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.
    作者回复

    👍

    2019-03-28 21:51:35

  • 海鸿

    2019-03-19 19:29:00

    如果线程处于阻塞状态(BLOCKED),此时调用线程的中断方法,线程会又如何反应?
    是否会像等待状态一样抛异常?
    还是会像运行状态一样被标记为已中断状态?
    还是不受到任何影响?
    麻烦老师解答一下😁
    作者回复

    阻塞态的线程不响应中断,并发包里的锁有方法能够响应中断

    2019-03-20 08:23:28

  • WhoAmI

    2019-03-24 18:56:58

    老师,Java调用阻塞API时,Java层面是runnable,那仍然占用CPU吗,此时此线程在操作系统中是什么状态呢?这个问题好几个人都在问,能详细解释下吗?
    作者回复

    不占cpu,操作系统里是阻塞状态。

    2019-03-24 22:00:10

  • cky.宇

    2019-11-04 23:31:56

    根据接口文档描述,stop()虽然也会中止线程并释放锁,但是没有提供一种补偿的机会,可能某个线程对共享对象进行了不完整的修改,此时如果stop后,该共享对象会继续被其他线程使用,造成线程安全问题。interrupt()则是提供给使用者一种被中断后补偿的机会,例如回滚之前的修改。个人理解是这样的,请老师指点一下。
    作者回复

    完全同意!

    2019-11-08 23:08:27

  • alias cd=rm -rf

    2019-03-19 10:10:39

    思考题,不能中断循环,异常捕获要放在while循环外面
    作者回复

    你这也是个办法

    2019-03-19 20:20:18

  • 缪文

    2019-04-02 23:07:27

    if(th.isInterrupted()) {
    break;
    }
    其实这段代码完全没必要啊,在捕获中断异常后,直接break就好了
  • Aven

    2019-07-02 10:28:17

    老师,您好,想问下,在讲到“java调用阻塞api的情况下,java程序仍然是Runnable状态”这里的时候,我不太理解,哪些api是属于阻塞api呢
    作者回复

    阻塞式的读文件,读网络数据都是

    2019-07-02 20:11:38

  • J.M.Liu

    2019-03-19 18:48:44

    感谢老师提醒,原来jvm层面的线程状态和os层面上的线程状态是不一样的,i/o挂起在jvm也是runable状态。另外并发包的lock其实是处于waitting状态。
    但是有个疑问,jvm中blocked状态的线程和waitting状态的线程,除了处在不同的队列之外,还有没有什么区别呀?我这里问的区别包括jvm和os两个层面,谢谢老师
    作者回复

    block不能响应中断,os里应该都是休眠状态,因为都不能获得cpu使用权

    2019-03-20 08:33:15

  • yang

    2019-03-20 12:16:21


    public class TestThread {

    public static void main(String[] args) throws InterruptedException {

    Worker t = new Worker();
    t.start();

    Thread.sleep(2000);

    System.out.println("-1-1-1-");
    t.interrupt();
    System.out.println("000000");
    Thread.sleep(2000);
    t.stop();
    System.out.println("000111");
    Thread.sleep(2000);
    t.join();
    System.out.println("111111");
    }

    }

    class Worker extends Thread {

    @Override
    public void run() {
    int i = 0;
    while (i<20) {
    if (Thread.currentThread().isInterrupted()) {
    break;
    }
    ++i;
    System.out.println(Thread.currentThread().getName() + "i: " + i);
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    }
    }
    }

    ----------------------------------------------------------------------------
    忽然发现极客时间网页版的留言窗口好小啊,都看不到自己上面写的东西...
    ----------------------------------------------------------------------------

    1. 如果worker中没有sleep方法,则调用th.interrupt()方法会真正的中断th线程,并且不会抛出InterruptException 但是该演示代码不能体现锁的释放;
    2. 如果worer中有sleep方法,则调用th.interrupt()方法会抛 java.lang.InterruptException(), 是针对sleep方法抛出的
    同样的Object的wait() wait(带参) 也会抛出java.lang.InterruptException()而从当前的wait/blocked状态被中断(唤醒)
    那也就是说,throws InterruptedException 的方法 在线程被调用interrupt()方法后,会被从当前状态中断
    至于调用interrupy()方法后线程的状态属于哪种,取决于interrupt方法前的执行的方法使得当前线程处于哪种状态,
    老师的总结很到位,需要好好理解,感受~!
    3. 无论worder的run中有没有slee()方法,stop都会直接中断线程,当前演示代码也无法演示锁没有被释放
    4. join()总是在等待被调用的线程执行完毕
    5. while循环放在try里面, 在调用th.interrupt之后,可以有效捕获InterruptException 从而使th线程中断

    说的有点多了, 大家多多讨论~!~!~!

    作者回复

    好认真👍👍👍

    2019-03-22 20:30:16

  • 2019-03-19 12:23:52

    老师 stop方法直接杀掉线程了,什么不会释放锁呢
    作者回复

    我也不知道搞jvm的人咋想的

    2019-03-19 20:12:36

  • ZOU志伟

    2019-03-22 18:03:56

    老师,我有个疑问,文章中讲到线程调用阻塞式 API 时,不会转换到 BLOCKED 状态,而是保持RUNNABLE状态,想知道这些阻塞式API是什么?
    作者回复

    java里的bio,例如读文件写socket

    2019-03-22 20:01:42

  • 向往的生活

    2019-03-19 15:15:41

    当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发 InterruptedException 异常。此时如果线程A获取不到锁,岂不是会立马又变成BLOCKED 状态?
    作者回复

    我估计不会有中间的runnable,只是换个队列而已

    2019-03-20 08:39:40

  • 拯救地球好累

    2019-07-22 20:43:12

    ---总结---
    1. JAVA的线程生命周期模型在操作系统的基础上稍作了修改,一是Runnable表示可运行与运行状态(等待资源依然是Runnable),二是将休眠状态扩展成了Blocked&Waiting&Time_waiting三种状态

    ---启发---
    1. 作为工程师能搞懂理论模型并能在其基础上按需求变化是个必备的能力
    2. 对使用的任何类,懂得其原理和设计理念能保证更好地使用
    3. 对使用的任何方法,懂得其操作方式和可能出现的异常也非常重要
    4. 养成看doc的习惯

    ---课后思考---
    sleep方法的java doc中写道“if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.”
    因此在try-catch中出现的中断异常被捕获后不做处理将会导致interruped status被清理。
    作者回复

    👍

    2019-10-10 09:21:50

  • 忍者无敌1995

    2019-04-22 10:56:40

    看了下留言,感觉自己的思考还可以深一点,应该接着问自己:如果这样不行,那应该采用什么样的写法呢,慢慢培养自己的思维习惯吧