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()方法,即使执行完成
- 在当前线程(假设为线程A)调用
-
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()方法在停止线程执行时,不保证线程的资源正常释放
- suspend()方法不释放已占有资源进入睡眠状态,所以线程会处于
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变量保证内存可见性,可用于线程退出标识
参考链接:
- Java结束线程的三种方法
- 对interrupted()作用于当前线程的贴心提示:线程中断:Thread类中interrupt()、interrupted()和 isInterrupted()方法详解
- java-线程中start和run的区别
- Java线程与操作系统线程的1:1关系:Kotlin 协程真的比 Java 线程更高效吗?