Java线程的状态转换 & 如何停止线程

1. Java线程的状态转换

1.1 Java线程的状态转换图

  • Java线程在整个生命周期可能处于6种不同的状态,但任意时刻只能处于一种状态
  • 随着代码的执行,Java线程会在不同的状态之间切换,具体如图所示

1.2 状态的说明

NEW状态

  • 新建一个线程对象,但还未调用start() 方法
  • 线程类:实现Runnable接口或继承Thread类得到的一个线程类

RUNNABLE状态

  • 线程对象被创建后,其他线程调用该线程的start()方法,该线程将处于RUNNABLE状态,位于可运行线程池中

  • Java线程中,将就绪态(READY)和运行态(RUNNING)统称为运行态(RUNNABLE)

    JVM中,处于RUNNABLE的线程可能在等待其他资源,如CPU
    也就是说,运行态还包含了就绪态

  • 可以看到,在Java的线程状态中,实际是不存在RUNNING和READY的

  • 统称为运行态的原因:

    • Java线程与操作系统线程是一一对应的,线程的调度实质是由操作系统决定的
    • JVM中的线程状态实质是对底层状态的映射及包装
    • 线程因为CPU时间片耗尽、CPU时间片被抢占,甚至主动让出CPU时间片,都将进入READY状态
    • RUNNING状态和READY状态之间的切换是非常现频繁的
    • 而Java线程状态是为监控服务的,二者切换如此之快,对其进行区分是没有意义的
    • 因此,将RUNNING状态和READY状态统称为RUNNABLE状态是一个不错的选择

READY状态

  • 处于RUNNABLE状态的线程,会由于等待资源而处于READY状态
  • 以下情况都可能使线程进入READY状态
    • NEW → \rightarrow READY:创建线程后,调用该线程的start()方法
    • RUNNING → \rightarrow READY:处于RUNNING状态的线程,CPU时间耗尽、CPU时间片被抢占、通过yield()方法主动让出CPU时间片
    • WAITING/ TIMED_WATING → \rightarrow READY:处于等待状态的线程,被通知或超时自动返回
    • BLOCKED → \rightarrow READY:由于等待对象锁而处于阻塞状态的线程,获取到对象锁

RUNNING状态

  • 处于可运行线程池中的线程,被线程调度程序选中(获得CPU时间片),将处于RUNNING状态

BLOKED状态

  • 线程进入synchronized方法或synchronized代码块时,需要等待获取对象锁,从而进入阻塞状态

WAITING状态

  • 由于执行某些方法,将使线程进入等待状态,需要被显式唤醒,否则将处于无限期等待
  • 处于等待状态的线程,不会被分配CPU时间片
  • 以下是进入和退出等待状态的方法
    在这里插入图片描述
  • 注意
    • 被动与主动: 阻塞状态,是线程为了获取对象锁被动"等待";等待状态,是线程为了等待某些条件的满足,主动地调用特定方法进行等待
    • 同样等待获取锁,不同的状态: 进入同步方法或同步代码块,线程处于阻塞状态;而执行lock()方法却是进入等待状态,因为lock()方法的实现基于LockSupport类中的相关方法

TIMED_WATING状态

  • 通过调用特定方法使线程进入等待状态后,若该线程未被显式唤醒,将一直等待
  • 因此,可以在等待状态的基础上增加超时限制,避免一直等待
  • 以下是进入和超时等待状态的方法
    在这里插入图片描述

TERMINATED状态

  • 线程的run()或call()方法执行结束或因为异常退出,线程将处于终止状态
  • 线程一旦终止了,就不能复生:若对终止的线程再次调用start()方法,将抛出java.lang.IllegalThreadStateException异常。
  • 注意: 主线程退出,子线程不会立即结束;除非,这些子线程为守护线程(JVM中不存在非守护线程,自动退出)

参考文档:

1.3 一些热点问题的回答

1.3.1 主线程结束,子线程立即结束?

错误的认知:主线程线程结束,子线程立即结束

  • 不知何时,自己对主线程和子线程有这样的印象:

    • main()方法中创建线程,如果不通过jion()、sleep()等方法,让主线程等待一段时间
    • 一旦主线程执行结束,子线程也会立即结束
    • 这里的结束,不是真的结束,而是标准输出中将不再出现子线程的打印信息。
    • 因此,我会认为主线程结束,子线程也结束了
  • 就像下面的代码:

    • 自己的记忆中:一旦主线程执行完最后一句打印操作,子线程的打印也将停止,给人一种子线程也结束的错觉
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
            }
        });
        thread.start();
        System.out.println("主线程结束");
    }
    
  • 和同事沟通,同事说自己也是这样的认知。

  • 但这次将代码一执行,发现主线程结束,子线程仍在继续打印 😂

认知错误

  • 仔细想想,上述的认知是不合常理的
  • Java程序中,任意线程最终肯定都是被自己的父线程创建的。
  • 父线程创建子线程,是为了执行某些特定任务
  • 子线程中的任务可能还在执行,就因为父线程结束,这些任务就被暴力结束了,那这样Java程序就乱套了
  • 因此,动动脑子想想,上述认知肯定不合常理

JVM何时退出

  • Java程序中,主线程(非守护线程)随着main()方法执行完毕而结束。
  • 若JVM中不存在其他非守护线程,JVM将退出
  • 此时,JVM中的所有守护线程都需要立即终止
  • 总结:
    • 一旦JVM中不存在非守护线程,JVM将退出。
    • 从而,会导致守护线程立即终止

若无其他非守护线程,主线程结束,守护线程将立即结束

  • 在main方法中,创建一个打印0~9的守护线程。一旦主线程退出,该守护线程也将停止打印

    public static void sleep(TimeUnit timeUnit, long timeout) {
        try {
            timeUnit.sleep(timeout);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                sleep(TimeUnit.MILLISECONDS, 5);
            }
        });
        // 设为守护线程
        thread.setDaemon(true);
        thread.start();
        // 等守护线程执行一段时间后,主线程再结束
        sleep(TimeUnit.MILLISECONDS, 10);
        System.out.println("主线程结束");
    }
    
  • 执行结果如下:

参考文档:

1.3.2 守护线程

  • 从JVM何时退出的定义来看,守护线程更适合做辅助线程,例如,中后台调度、指标采集

  • 线程创建后,可以通过Threa.setDaemon()方法将其设置为守护线程

  • 注意:

    • 守护线程的设置,需要在启动线程(start()方法)之前完成
    • 由于守护线程的特殊性,不能依靠finally语句来实现收尾工作
  • 上面的守护线程示例代码,加上如下finally语句块,但finally语句块中的代码最终不会被执行

    finally {
        System.out.println("守护线程将退出,开始回收资源");
    }
    

1.3.3 yield()方法的作用?

对yield的理解

  • 线程主动调用yield()方法,可以从RUNNING状态转为READY状态
  • yield()方法究竟有何作用,其实自己一点也不了解
  • 英文单词yield的中文释义有:让步、放弃、屈服
  • 根据之前对线程状态的学习,处于RUNNING状态的线程占据了CPU时间片,处于READY状态的线程需要等待其他资源,包括CPU时间片
  • 因此,yield在yield()方法中的释义为让步放弃更加合理

yield()方法

  • yield()方法的定义如下:它是一个静态的、native方法

    public static native void yield();
    
  • 源码中该方法的注释翻译大概如下

    • 通过调用yield()方法,提示调度程序当前线程愿意让出自己的处理器资源
    • 调度程序可以随意忽略该提示:最终被调度程序选择执行的线程包括它自己,看起来就像没让步一样
    • yield是一种启发式尝试,旨在改善线程的相对进展,避免某些线程过度使用CPU
    • yield更加适合在调试或测试时使用
  • 总结:

    • 通过调用yield()方法,当前线程让出自己占有的CPU,让其他线程(包括自身)使用
    • 这更加适合当前线程已经完成重要工作,可以暂缓执行的情况

参考链接:

1.4 编程体会Java线程的状态变化

1.4.1 普通线程的"生与死"

  • 如果不存在获取锁、主动sleep、wait等特殊操作,线程的运行状态:NEW → \rightarrow RUNNABLE → \rightarrow TERMINATED
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("子线程开始运行,子线程状态: " + Thread.currentThread().getState());
        });
        System.out.println("创建一个子线程,子线程状态: " + thread.getState());
        // 启动子线程
        thread.start();
        // 等待子线程执行结束
        sleep(TimeUnit.SECONDS, 1);
        System.out.println("子线程执行结束,子线程状态: " + thread.getState() + ", isAlive: " + thread.isAlive());
    }
    
  • 执行结果如下,与预期一致

1.4.2 阻塞&超时等待

  • 下面的代码主要展示了线程的阻塞和超时等待两种状态
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Blocked(), "threadA");
        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        Thread thread2 = new Thread(new Blocked(), "threadB");
        thread2.start();
    
        // 线程A由于sleep而进入超时等待状态
        System.out.println(thread1.getName() + "由于sleep而进入超时等待状态:" + thread1.getState());
        // 线程B由于等待锁而阻塞
        System.out.println(thread2.getName() + "由于等待锁而阻塞:" + thread2.getState());
    }
    
    class Blocked implements Runnable {
        @Override
        public void run() {
            synchronized (Blocked.class) {
                System.out.println(Thread.currentThread().getName() + "获取到锁,处于" +
                        Thread.currentThread().getState() + "状态");
                // 获取到锁后,休眠一段时间
                sleep(TimeUnit.SECONDS, 10)
            }
        }
    }
    
  • 执行结果如下,与预期一致

2. 启动或终止线程

2.1 线程的创建

  • 通过Thread类的构造函数创建一个线程,最终都将调用私有的init()方法初始化线程

  • init方法比较复杂,下面只展示关键代码

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // 设置线程名
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        // 线程名,默认为Thread-xx
        this.name = name;
    	// 创建该线程的当前线程作为父线程
        Thread parent = currentThread();
        // 省略与SecurityManager有关的代码
    	
    	// 线程组中,未启动的线程加1
        g.addUnstarted();
    	
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        // 代码有省略
        this.target = target;
        setPriority(priority);
        // 代码有省略
        this.stackSize = stackSize;
    	// 线程ID,从1开始递增
        tid = nextThreadID();
    }
    
  • 从init的代码来看,初始化线程,需要设置线程名、线程组、是否为守护线程、优先级、Runnale对象、栈大小、线程ID等

  • 默认继承父线程的优先级、是否为守护线程,需要通过方法setPriority()、setDaemon()设置线程自己的优先级、是否为守护线程

2.2 线程的启动

2.2.1 start方法

  • 从Java线程的状态转换可知,新创建的线程想要运行起来必须通过start()方法启动

  • start()方法的注释如下

    • 在当前线程(假设为线程A)调用threadB.start(),可以使线程B开始运行,JVM会调用线程B中的run()方法
    • 此时,线程A和线程B并发执行
    • 不能反复调用一个线程的start()方法,即使执行完成
  • start()方法代码如下:

    public synchronized void start() {
        // 0表示线程处于NEW状态,只要不是NEW状态
        // 就说明线程已经启动,会抛出IllegalThreadStateException
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        // 记录线程组中已经启动的线程,未启动线程数nUnstartedThreads减1
        group.add(this);
    
        boolean started = false;
        try {
            start0(); // 调用native方法启动线程
            started = true; // 成功启动
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this); // 线程启动失败,退回到未启动线程 ”队列“
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    
  • native的start0()方法最终将调用JVM_StartThread()方法 → \rightarrow new JavaThread() → \rightarrow os::createThread() → \rightarrow pthread_create()

2.2.2 run() vs start()

  • Thread类的定义如下,它实现了Runnable接口

    public class Thread implements Runnable
    
  • 按照之前的描述,线程启动以后,将执行run()方法中的代码

  • Thread对Runnable接口的run()方法实现如下,实际是执行传入Runnable类型的target的run()方法

    public void run() {
        if (target != null) {
            target.run();
        }
    }
    
  • 疑问: 既然Thread类本身就存在run()方法,为何不直接调用run()方法?

  • 自己的猜想: start()方法在操作系统层面创建了一个线程,才得以支持多线程的执行;而run()方法并未创建新的线程,其代码的执行是在当前线程中完成的

  • 验证: run()方法是在当前线程中执行的

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("执行run()方法的线程:" + Thread.currentThread().getName());
            for (int i = 0; i < 5; i++) {
                System.out.println(i);
                sleep(TimeUnit.MILLISECONDS, 1);
            }
        });
        thread.run();
        System.out.println("执行主线程代码,子线程的状态为" + thread.getState());
    }
    
  • 通过程序运行结果可知:

    • 直接调用线程的run()方法,其代码在当前线程中执行。
    • run()方法执行完后,才会继续执行当前线程的后续代码
  • start()和run方法差异,总结如下

    • start()方法可以创建并启动一个新线程,并在新线程中执行run代码,不影响当前线程后续代码的执行 —— start() 方法实现了多线程
    • run()并未新建线程,直接在当前线程执行run代码。run代码执行完,才能继续执行当前线程的后续代码 —— run() 方法没有实现多线程
    • start()方法只能调用一次,再次调用将抛出IllegalThreadStateException 异常;run()方法可以调用多次

2.3 suspend()、resume()、stop()线程

  • 就像以前初中使用的复读机一样,有时我们希望某个线程能暂停执行,然后恢复执行,甚至希望线程直接停止执行

  • suspend()方法可以让线程暂停执行,resume()方法可以让线程恢复执行,stop()方法可以让线程停止执行

    public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Thread thread = new Thread(() -> {
            while (true) {
                // 每隔1秒,说一次hi
                sleep(TimeUnit.SECONDS, 1);
                System.out.println(format.format(new Date()) + "\t hi");
            }
        });
        thread.start();
        // 执行一段时间,暂停线程
        sleep(TimeUnit.SECONDS, 3);
        thread.suspend();
        System.out.println(format.format(new Date()) + " 主线程暂停子线程, 子线程状态:" + thread.getState());
        // 3秒后,恢复子线程
        sleep(TimeUnit.SECONDS, 3);
        thread.resume();
        System.out.println(format.format(new Date()) + " 主线程恢复子线程, 子线程状态:" + thread.getState());
        // 3秒后,停止子线程
        sleep(TimeUnit.SECONDS, 3);
        thread.stop();
        System.out.println(format.format(new Date()) + " 主线程停止子线程");
        sleep(TimeUnit.SECONDS, 1);
        // 刚stop就打印,可能还未完成stop,打印出来是RUNNABLE
        System.out.println("子线程状态:" + thread.getState());
    }
    
  • 执行结果如下:由于多线程执行,主线程恢复子线程和子线程的打印可能交换顺序

  • 从执行结果可以看出,以上方法成功实现了线程的暂停、恢复和停止

  • 在IDE中,显示这些方法是废弃的方法,不建议使用

    • suspend()方法不释放已占有资源进入睡眠状态,所以线程会处于TIMED_WAITING状态
    • stop()方法在停止线程执行时,不保证线程的资源正常释放

2.4 interrupt()停止线程

2.4.1 interrupt()方法

  • 在计算组成原理类似的课程中,经常提到中断、中断响应等

  • 在线程A中调用threadB.interrupt()方法,就类似线程A对线程B打了一个招呼,线程B可以通过检查自己是否被中断来进行响应

  • 是否被中断,可以通过isInterrupted()方法来进行判断。该方法返回true,表示被中断

  • 例外情况:

    • 如果线程由于join、sleep、wait等不可中断的操作中,在抛出InterruptedException之前,中断标识会被清除
    • 此时,isInterrupted()方法将返回false
  • interrupt()的方法注释中,还讲了关于I/O操作的中断情况,有需要可深入了解

  • 下面的代码,展示了对不同线程调用interrupt()方法后,中断标识的状态

    public static void sleep(TimeUnit timeUnit, long timeout) {
        try {
            timeUnit.sleep(timeout);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "被中断, 中断标识: " + Thread.currentThread().isInterrupted());
            // 中断后停止当前线程
            Thread.currentThread().stop();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            while (true) {
                sleep(TimeUnit.SECONDS, 1);
            }
        }, "线程A");
        Thread thread2 = new Thread(() -> {
            while (true) {
                for (int j = 0; j < 100000000; j++) {
                    for (int i = 0; i < 1000000000; i++) {
    
                    }
                }
                System.out.println("完成计数一次");
            }
        }, "线程B");
    
        thread1.start();
        thread2.start();
    
        // 中断线程
        sleep(TimeUnit.MILLISECONDS, 200);
        thread1.interrupt();
        thread2.interrupt();
        // 线程A由于sleep被中断而停止执行,线程B不受影响,继续执行
        sleep(TimeUnit.MILLISECONDS, 100);
        System.out.println(thread1.getName() + "状态:" + thread1.getState() + ", isInterrupted: "
                + thread1.isInterrupted() + ", " + thread2.getName() + "状态:" + thread2.getState()
                + ", isInterrupted: " + thread2.isInterrupted());
        thread2.stop();
    }
    
  • 从执行结果可以看出

    • 处于sleep的线程在抛出InterruptedException异常前,中断标识被清除
    • 中断操作不影响线程的执行
      在这里插入图片描述

2.4.2 通过中断标识停止线程

  • 从上面的示例可以看出,普通线程被中断后,中断标识为true

  • 可以利用这个特性,停止线程执行,例如:从循环中退出

    Thread thread = new Thread(() -> {
        Thread currentThread = Thread.currentThread();
        while (!currentThread.isInterrupted()) {
            // do something
        }
        // 中断后退出循环,停止执行
        System.out.println(currentThread.getName() + "中断: "+ currentThread.isInterrupted()  + ", 停止执行");
    }, "thread1");
    thread.start();
    sleep(TimeUnit.SECONDS, 1);
    // 中断线程
    thread.interrupt();
    

2.4.3 如何继续响应下次中断

  • 有时,我们希望线程被中断后,可以执行某些操作以响应中断,然后继续工作、响应中断

  • 这就需要响应中断后,清除中断标识,以保证能继续响应中断

  • 可以使用interrupted()方法获取当前线程的中断标识并清除中断标识,以继续响应中断

  • 代码示例如下:通过interrupted()方法可以多次响应中断

    Thread thread = new Thread(() -> {
        while (true) {
            if (Thread.interrupted()) { // 中断标识为true,响应中断
                System.out.println(Thread.currentThread().getName() + "中断: " + Thread.currentThread().isInterrupted());
            } else {
                // do other things
            }
        }
    }, "thread1");
    thread.start();
    sleep(TimeUnit.SECONDS, 1);
    // 中断线程
    thread.interrupt();
    sleep(TimeUnit.SECONDS, 1);
    // 继续中断线程,中断可以被响应
    thread.interrupt();
    
  • 执行结果

  • 细心的你会发现:interrupted()方法是静态方法,因为它的作用是获取并清除当前线程!!!的中断标识

2.5 通过共享变量停止线程

  • volatile变量保证内存可见性,可以通过volatile类型的共享变量及时停止线程

    class MyThread extends Thread {
        public volatile boolean exit = false;
    
        @Override
        public void run() {
            while (!exit) {
                // do something
            }
            System.out.println("条件满足,完成首尾工作后,线程将停止");
        }
    }
    
    public static void main(String[] args)  {
        MyThread thread = new MyThread();
        thread.start();
        // 运行一段时候,通过共享变量中断线程
        sleep(TimeUnit.SECONDS, 1);
        thread.exit = true;
    }
    

3. 总结

  • 此次,主要学习了线程的状态转换、线程的启动与停止

线程的状态转换

  • Java线程实际只有6种状态,其中RUNNABLE状态包含RUNNIG和READY两种状态
  • Java线程状态转换,例如,何时进入或退出等待/超时等待状态,何时进入READY状态,何时进入或退出阻塞状态等

一些小知识

  • yield()方法:当前线程自愿让出CPU时间片,让其他(包括自己)使用
  • Java线程之间互不相干:父线程结束,子线程不受影响
  • JVM的退出与是否存在非守护线程有关系:只要不存在非守护线程,JVM退出

线程的启动

  • 新建线程,只是初始化了相关参数,并未真正的在操作系统层面创建线程
  • start()方法:调用native方法,最终在操作系统层面创建线程;不可重复调用
  • start()方法 vs run()方法:前者可以实现多线程,不可重复调用;后者无法实现多线程,可重复调用

线程停止的方法

  • 通过不完善的stop()方法
  • 通过interrupt()方法 + isInterrupted()方法:
    • interrupt() 方法:若线程在执行中断的操作,中断标识被清除
    • isInterrupted()方法:线程被中断,则返回true
    • 特殊的 interrupted()方法:返回中断情况并清除中断标识,可用于重复响应中断
  • 通过共享变量:volatile变量保证内存可见性,可用于线程退出标识

参考链接:

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值