目录
前言
状态转移图
1.0 新建态到就绪态
1.1 就绪态到运行态
1.2 运行态到就绪态
1.2.1 时间片用完
1.2.2 t1.yield() ,Thread.yield();
1.3 运行态到阻塞态
- 1.3.1 Thread.sleep()
- 1.3.2 t2.join()
1.3.3 t1 等待用户输入, 等待键盘响应
1.4 阻塞态到就绪态
1.5 运行态到等待队列
1.6 运行态到锁池队列
1.7 等待队列到锁池队列
1.8 锁池队列到就绪态
1.9 运行态到死亡态
前言
看到网上关于线程状态转移的博客, 好多都没说明白. 查了很多资料, 汇总一篇, 希望通过这一篇, 能把这些状态转移解释明白, 如果有什么没考虑到的, 希望指正
状态转移图
要明白线程转移的详细过程, 可以先通过一张图片, 了解一个线程的生命周期中, 该线程会处在何种状态:
注意: 单向箭头表示不可逆
1.0 新建态到就绪态
概念: 1. 新建态: 一个线程被创建出来时候所处的状态 ;2. 就绪态: 线程调用 start() 方法后, 便处于可以被操作系统调度的状态, 即就绪态. 该状态可以由三处转化而来, 新建态执行了 start, 线程阻塞结束, 锁池等待队列中的线程获得了锁
- Thread t1 = new Thread(
- new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i <10; i++) {
- System.out.println("hello :" + i);
- }
- }
- }
- );
- // t1 执行 start() 之后, 处于就绪态, 操作系统此时可以分配时间片给该线程, 让该线程执行 run 方法体中的内容
- t1.start();
该状态对应状态图中的第一步, 比较简单, 不再赘述
1.1 就绪态到运行态
概念: 运行态: 表示当前线程被操作系统调度, 分配了时间片, 执行线程中的 run 方法时的状态. 运行态只可以由就绪态的线程转化而来, 如果多个线程都处在就绪态, 就等待操作系统分配
- public static void main(String[] args) {
- // 线程 1
- Thread t1 = new Thread(() -> {
- for (int i = 0; i <10; i++) {
- System.out.println("t1 : running");
- }
- });
- t1.start();
- // 线程 2
- Thread t2 = new Thread(() -> {
- for (int i = 0; i <10; i++) {
- System.out.println("t2 : running");
- }
- });
- t2.start();
- }
注: 可以看到 t1 和 t2 两个线程都运行 start() 方法后, 控制台会随机交叉打印两个线程的输出信息, 这种随机, 是操作系统随机分配时间片的调度决定的
1.2 运行态到就绪态
1.2.1 时间片用完
我们知道, 操作系统为了公平, 不可能从就绪态里面选择一个, 一直执行完, 而是随机切换到另外的线程去执行, 每个线程分配的执行时间结束, 操作系统去调用别的线程, 当前刚执行结束的线程便由运行态重新回到就绪态, 等待操作系统的再次分配. 参考上一个代码例子, t1 的线程执行体方法中循环打印 100 次, t2 也是, 但是会看到控制台是交叉打印的, 说明了这一点
1.2.2 t1.yield() ,Thread.yield();
概念: 在 t1 线程体中调用 t1.yield(), 和 Thread.yield(); 本质上一样, Thread.yield() 表示当前线程让渡. 线程调用 yield() 方法, 会让该线程重新回到就绪队列, 但是 yield() 让当前线程回到就绪队列后, 并不能保证操作系统再次调用不会选择该线程, 所以 yield() 方法不能用来控制线程的执行顺序
- public static void main(String[] args) {
- // 线程 1
- Thread t1 = new Thread(() -> {
- Thread.yield();
- for (int i = 0; i <10; i++) {
- System.out.println("t1 : running" + i);
- }
- });
- t1.start();
- // 线程 2
- Thread t2 = new Thread(() -> {
- for (int i = 0; i <10; i++) {
- System.out.println("t2 : running" + i);
- }
- });
- t2.start();
- }
注意: 这个程序我故意把线程让步 yield() 方法写在线程体刚运行的时候, 也就是说, 每次操作系统分配给 t1 线程时间片时候, t1 都会让步. 但这次的让步不代表 t1 接下来的方法不会执行, 也就是我让步之后, 大家再一起抢, t1 又抢到了时间片, 那么 t1 本次时间片内便执行接下来的方法, 等时间片结束, 再次分配 t1 时间片, t1 还会让, 再接着抢, 抢到和抢不到都有可能.
1.3 运行态到阻塞态
概念: 阻塞态表示当前线程被由于某种原因, 被挂起, 也就是被阻塞, 正在运行的线程被阻塞后, 即使结束阻塞状态也回不去运行态, 只能回到就绪态, 等待 os 分配 CPU 资源去调度
- 1.3.1 Thread.sleep()
- public static void main(String[] args) {
- Thread t1 = new Thread(() -> {
- for (int i = 0; i <10; i++) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("hello :" + i);
- }
- }
- );
- // t1 执行 start() 之后, 处于就绪态, 操作系统此时可以分配时间片给该线程
- t1.start();
- }
注意: 让当前线程睡眠, 该线程被阻塞, 睡眠时间结束, 该线程接着运行
1.3.2 t2.join()
当在 t1 中调用 t2.join(). 那么 t1 会阻塞, 一直等待 t2 执行完毕, 才结束阻塞回到就绪态
直接看代码: 这里我把 t1 和 t2 抽出来当做全局静态变量
- public class TestThread {
- static Thread t1;
- static Thread t2;
- public static void main(String[] args) {
- // 线程 1
- t1 = new Thread(() -> {
- for (int i = 0; i <100; i++) {
- if(i == 50) {
- try {
- t2.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- System.out.println("t1 : running" + i);
- }
- });
- t1.start();
- // 线程 2
- t2 = new Thread(() -> {
- for (int i = 0; i <100; i++) {
- System.out.println("t2 : running" + i);
- }
- });
- t2.start();
- }
- }
解释: 这个程序的运行结果是, 首选 t1,t2 挣抢时间片, 按系统调度, 首先控制台 t1 和 t2 都有打印自身的输出信息, 当 t1 执行到 i=50 的时候, 调用了 t2.join(). 此时控制台会全部打印 t2 的信息, 一直等待 t2 的循环结束, 执行体的 run 方法结束, 再去打印 t1 剩下的没运行完的循环
所以 join 的流程可以抽象为下面这张图片
1.3.3 t1 等待用户输入, 等待键盘响应
这个很好理解, 比如你就执行一个 main 函数的主线程, 等待输入时, 该线程是不会结束的, 就是处于阻塞状态.
1.4 阻塞态到就绪态
1.3 中所有阻塞态结束, 比如 sleep 结束, join 后 t2 执行结束, 用户输入了信息回车等. t1 会结束阻塞态, 但是都是回到就绪态, 无法再立即回到运行态
1.5 运行态到等待队列
这里牵扯到对象锁的概念
两个线程竞争锁, 其中 t1 释放锁, 也就是把所占有的对象锁让出. 那么如果不主动唤醒, 该线程一直处在等待队列中, 得不到操作系统 OS 的调度
概念: 等待队列, 就是当前线程占有锁之后, 主动把锁让出, 试自身进入等待队列. 此种 wait 加 notify 可以保证线程执行的先后顺序. notify() 是通知一个等待队列的线程回到锁池队列. notifyAll() 是通知所有处在等待队列的线程, 都回到锁池队列.
- show me code:
- public static void main(String[] args) {
- Object o = new Object();
- // 线程 1
- Thread t1 = new Thread(() -> {
- synchronized (o) {
- for (int i = 0; i <10; i++) {
- try {
- if(i == 5) {
- // 当 i=5 的时候, 让出对象锁, t1 进入等待队列
- // 如果没人通知, t1 一直等待, 程序不会结束
- o.wait();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("t1 : running" + i);
- }
- }
- });
- t1.start();
- // 线程 2
- Thread t2 = new Thread(() -> {
- synchronized (o) {
- for (int i = 0; i < 10; i++) {
- System.out.println("t2 : running" + i);
- }
- // 这里 t2 得到锁, 执行完线程方法之后一定要通知 t1 停止等待.
- // 不然 t1 结束不了, 处在一直等待通知的状态
- o.notify();
- }
- });
- t2.start();
- }
1.6 运行态到锁池队列
参考 1.5 的程序, 在 i=5 之前, t1 占有该对象锁, t2 即使 start() 也得不到运行, 原因是该对象锁被 t1 占有, t2 拿不到, 所以就进入锁池队列
1.7 等待队列到锁池队列
参考 1.5 的程序, 当 t1wait 之后, 让出对象锁, t1 进入了等待队列, t2 拿到锁, 运行完之后, 调用 notify() 让等待队列中的 t1 进入锁池队列.
1.8 锁池队列到就绪态
参考 1.5 的程序, 当 t2 结束后, 通知 t1 进入锁池队列, t2 由于运行结束, 处在锁池队列中的 t1 可以拿到对象锁, 进入就绪态, 等待操作系统的调度, 从而进入运行态
1.9 运行态到死亡态
死亡态不可逆, 一旦线程进入死亡态, 就再也回不到其他状态
死亡态只能由运行态进入, 运行态中的线程. 例如通过操作系统的不停调度, t1 直到把整个 run 方法中的循环体执行完毕, 该线程完成了它的使命, 便进入死亡态
来源: https://www.cnblogs.com/darope/p/12748184.html