图解Java线程的生命周期模型

通用的线程生命周期模型

先来了解下通用的线程生命周期模型,这部分内容也适用于很多其他编程语言。
在这里插入图片描述
通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
在这里插入图片描述

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

Java 语言里把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。同时 Java 语言里细化了休眠状态。

Java中线程的生命周期

先看看JDK1.8中,代表线程状态的类 State 的源码。
在这里插入图片描述

从源码中,我们可以看出 Java 中线程的生命周期有以下 6 种:

  • NEW(初始化状态)
  • RUNNABLE(可运行 / 运行状态)
  • BLOCKED(阻塞状态)
  • WAITING(无时限等待)
  • TIMED_WAITING(有时限等待)
  • TERMINATED(终止状态)

这看上去挺复杂的,状态类型也比较多。但其实在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。所以 Java 线程的生命周期可以简化为下图:
在这里插入图片描述
NEW、TERMINATED 和 RUNNABLE 状态是之间的转换:

RUNNABLE 与 BLOCKED 的状态转换

  1. 只有一种场景会触发这种转换,就是线程等待 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() 方法。(注意要先获取锁)
    • 第二种场景,调用无参数的 Thread.join() 方法。例如有一个线程对象 thread A,当调用 A.join() 的时候,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE(Thread.join() 底层就是用 Thread.wait() 实现的,且 Thread.join() 方法上加了 synchronized)。
         public final synchronized void join(long millis)
          throws InterruptedException 
      
    • 第三种场景,调用 LockSupport.park() 方法。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 状态的区别,仅仅是触发条件多了超时参数。

从 NEW 到 RUNNABLE 状态

Java 刚创建出来的 Thread 对象就是 NEW 状态,而创建 Thread 对象主要有两种方法。

  1. 一种是继承 Thread 对象,重写 run() 方法。
  2. 另一种是实现 Runnable 接口,重写 run() 方法,并将该实现类作为创建 Thread 对象的参数。

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

从 RUNNABLE 到 TERMINATED 状态

线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。

RUNNABLE 中运行状态到可运行状态

  1. yield()
        /**
         * A hint to the scheduler that the current thread is willing to yield
         * its current use of a processor. The scheduler is free to ignore this
         * hint.
         * /
    
    放弃CPU时间片,让操作系统进行一次进程调度,操作系统可以不理。
  2. sleep(0)
    在规范上,并没有规定Thread.sleep(0) 和 Thread.yield()这俩方法有怎样的行为:也就是说,具体行为取决于JVM实现,不同的虚拟机可以有不同的实现。我们以 HotSpot JDK 8 为例
    • 对于Thread.sleep(0)
      JVM中有一个选项ConvertSleepToYield(默认为true),
      true表示的意思是:如果是sleep(0),就等同于执行Thread.yield()。
      false表示的意思是:不调用yield,而是直接 sleep 1ms
    • 对于Thread.yield()
      JVM中有一个选项ConvertYieldToSleep(默认为false)
      true表示的意思是:转换成sleep去实现,并睡1ms
      false表示的意思是:走的是yield的逻辑

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值