前言
线程是操作系统中的一个概念, 支持多线程的语言都是对 OS 中的线程进行了封装. 要学好线程, 就要搞清除它的生命周期, 也就是生命周期各个节点的状态转换机制. 不同的开发语言对操作系统中的线程进行了不同的封装, 但是对于线程的声明周期这部分基本是相同的. 下面先介绍通用的线程生命周期模型, 然后详细介绍 Java 中的线程生命周期以及 Java 生命周期中各个状态是如何转换的.
通用的线程生命周期
上图为通用线程状态转换图(五态模型).
初始状态
线程被创建, 但是还不允许分配 CPU 执行. 这里的创建仅仅是指在编程语言层面被创建; 在 OS 层面还没有被创建.
可运行状态
线程可以分配 CPU 执行. 在这种状态下, 真正的 OS 线程已经被成功创建, 所以可以分配 CPU 执行.
运行状态
当有空闲的 CPU 时, OS 就会将空闲 CPU 分配给一个处于可运行状态的线程, 被分配到 CPU 的线程的状态就转换成了运行状态.
休眠状态
运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量), 那么线程的状态就会转到休眠状态, 此时会释放 CPU 使用权, 休眠状态的线程永远没有机会获得 CPU 的使用权. 当等待的事件出现了(线程被唤醒), 线程就会从休眠状态转到可运行状态.
终止状态
程序执行完成或者出现异常就会进入此状态. 终止状态的线程不会切换到其他任何状态, 进入终止状态也就意味着线程的生命周期结束了.
以上五种状态在不同的编程语言中会简化合并 (C 中 POSIX Thread 规范将初始状态和可运行状态合并) 或者细化(Java 中细化了休眠状态).Java 中将可运行状态和运行状态合并了, Java 虚拟机不关心这两个状态, 把线程的调度交给了操作系统.
Java 线程的生命周期
Java 语言的线程共有六种状态: New(初始化状态),RUNNABLE(可运行状态 / 运行状态),BLOCKED(阻塞状态),WAITING(无时限等待),TIMED_WAITING(有时限等待),TERMINATED(终止状态).
在操作系统层面, Java 线程中的 BLOCKED, WAITING ,TIMED_WAITING 都是休眠状态. 只要 Java 处于这三种状态之一, 那么这个线程就永远没有 CPU 使用权.
下面是 Java 线程的状态转换图:
这六种状态之间的转换, 注意箭头的方向, 哪些状态是可以互转的哪些是不可以互转.
RUNNABLE --> BLOCKED
只有一种场景会触发这种转换, 即线程等待 synchronized 内置锁. synchronized 关键修饰的方法, 代码块同一时刻只允许一个线程执行, 其他未能执行的线程则等待. 这种情况下, 等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态. 当等待的线程获得内置锁时, 就会从 BLOCKED 转换到 RUNNABLE 状态.
线程调用阻塞式 API 时, 在操作系统层面线程是会转到休眠状态, 但是在 Java 虚拟机层面, Java 线程的状态是不会发生变化的, 会保持 RUNNABLE 状态. Java 虚拟机层面并不关心操作系统相关调度状态, 在它眼里, 等待 CPU 使用权 (OS 层面处于可执行状态) 和等待 I/O(OS 层面处于休眠状态)没有区别, 都是在等待某个资源, 所以都归入了 RUNNABLE 状态.
所以, 平时说 Java 在调用阻塞式 API 时, 线程会阻塞, 指的是操作系统线程的状态, 并不是 Java 线程的状态.
RUNNABLE --> WAITING
有三种场景会触发这种转换:
获取 synchronized 内置锁的线程, 调用无参数的 Object.wait()方法.
当前线程调用 wait()方法会将自己阻塞, 状态就从从 RUNNABLE 转到 WAITING 状态. 使用同一内置锁的其他线程可调用 notifyAll()唤醒阻塞在该锁上的所有线程, 此时被阻塞的线程状态就会从 WAITING 转到 RUNNABLE 状态.
调用 Thread.join()方法.
一个线程对象 thread A, 当调用 A.join()的时候, 执行这条语句的线程会等待 thread A 执行完, 而等待的这个线程, 其状态就会就会从 RUNNABLE 转到 WAITING 状态. 当 thread A 执行完, 原来的这个等待线程就会从 WAITING 状态转到 RUNNABLE 状态.
调用 LockSupport.park()方法.
Java 并发包中的锁都是基于 LockSupport 对象实现的. 调用 LockSupport.park()的当前线程会被阻塞, 线程的状态会从 RUNNABLE 转到 WAITING 状态. 调用 LockSupport.unpark(Thread t)可唤醒被阻塞的目标线程, 目标线程的状态就会从 WAITING 转到 RUNNABLE 状态.
RUNNABLE --> TIMED_WAITING
以下场景将会触发这个状态转变:
调用带超时参数的
Thread.sleep(long millis)
方法.
获得 synchronized 内置锁的线程, 调用带超时参数的
Object.wait(long timeout)
方法;
调用带超时参数的
Thread.join(long millis)
方法;
调用带超时参数的
LockSupport.parkNanos(Object blocker, long deadline)
方法;
调用带超时参数的
LockSupport.parkUntil(long deadline)
方法.
较与 WAITING 状态触发条件多了超时参数.
NEW --> RUNNALE, 创建线程的两种方式
Java 刚创建出来的 Thread 对象就是 NEW 状态, 而创建 Thread 对象主要有两种方法.
一种是继承 Thread 对象, 重写 run()方法.
- // 自定义线程类
- class MyThread extends Thread {
- @Override
- 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 状态. 那么如何转到 RUNNABLE 状态呢? 那就需要线程启动, 即调用线程的 start()方法.
- MyThread myThread = new MyThread();
- // 从 NEW 状态转换到 RUNNABLE 状态
- myThread.start();
- RUNNABLE--> TERMINATED
线程执行完 run()方法后, 会自动转换到 TERMINATED 状态.
如果执行 run() 方法的时候异常抛出, 也会导致线程终止. 有时候我们需要强制中断 run() 方法的执行, 可以发现 Java 的 Thread 类里面倒是有个 stop()方法, 但是该方法被标记为 @Deprecated, 已经被弃用了. 所以, 正确的方式是调用 interrupt() 方法.
stop()方法和 interrupt()方法的主要区别:
stop()方法会直接杀死线程. 如果线程持有 ReentrantLock 锁, 被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁, 那其他线程将再也没机会获得 ReentrantLock 锁. 这将会导致非常糟糕的结果. 所以该方法已经被废弃.
而 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 的 interrupt() 方法, 那么线程 A 可以通过 isInterrupted() 方法, 检测是不是自己被中断了.
小结
线程的生命周期以及各个状态的转换要好好掌握, 这对于调试 bug 还是很有用的.
参考:
[1]极客时间专栏王宝令《Java 并发编程实战》
来源: https://www.cnblogs.com/myworld7/p/12241842.html