深入理解Java线程状态转移

前言

看到网上关于线程状态转移的博客,好多都没说明白。查了很多资料,汇总一篇,希望通过这一篇,能把这些状态转移解释明白,如果有什么没考虑到的,希望指正

转载注明出处原文地址://www.cnblogs.com/darope/p/12748184.html

状态转移图

  • 要明白线程转移的详细过程,可以先通过一张图片,了解一个线程的生命周期中,该线程会处在何种状态:

    注意:单向箭头表示不可逆

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方法中的循环体执行完毕,该线程完成了它的使命,便进入死亡态