一、概述
记录时间 [2024-11-08]
前置知识:Java 基础篇;Java 面向对象
多线程 01:Java 多线程学习导航,线程简介,线程相关概念的整理
多线程 02:线程实现,创建线程的三种方式,通过多线程下载图片案例分析异同(Thread,Runnable,Callable)
多线程 03:知识补充,静态代理与 Lambda 表达式的相关介绍,及其在多线程方面的应用
Java 多线程学习主要模块包括:线程简介;线程实现;线程状态;线程同步;线程通信问题;拓展高级主题。
本文讲述线程状态相关知识,包括线程的五个基本状态,线程如何进行状态转换,以及如何在程序中观察线程的状态。
同时,文中介绍了几种线程使用方法,如线程停止、休眠、礼让等,还有线程优先级的设置和获取,以及守护线程的相关知识。
二、线程状态
1. 五个基本状态
在多任务操作系统中,线程在其生命周期内会经历不同的状态。虽然不同操作系统对线程状态的定义可能有所不同,但大多数操作系统都会定义以下五个基本状态:
- 新建(New):当一个线程被创建但尚未开始执行时,它处于新建状态。在这个阶段,线程对象已经存在,但是还没有分配给处理器时间片来执行。
- 就绪(Ready/Runnable):当一个线程准备好了,等待被调度执行时,它处于就绪状态。这意味着该线程已经准备好运行,只是当前没有获得 CPU 的时间片。一旦调度器选择这个线程,它将进入运行状态。
- 运行(Running):当一个线程正在执行其代码时,它处于运行状态。这是指线程当前被分配了 CPU 时间,并且正在执行指令。
- 阻塞(Blocked/Waiting):如果一个线程暂时不能继续执行,因为它正在等待某个事件的发生(如 I/O 操作完成、获取锁等),那么它就会进入阻塞状态。处于阻塞状态的线程不会消耗 CPU 时间,直到等待的条件满足后才会回到就绪状态。
- 死亡(Terminated/Dead):当一个线程完成了它的任务或因异常而终止时,它会进入死亡状态。一旦线程到达这个状态,它就不能再被调度执行。操作系统会回收该线程占用的资源。
2. 状态转换
这五个基本状态之间存在一定的联系,可以进行一些状态转换。
3. 观察线程状态
查阅 JDK 帮助文档
查阅 JDK 帮助文档可知,Thread.State
中描述了线程可以处在以下状态之一:
NEW
- 尚未启动的线程处于此状态;RUNNABLE
- 在 Java 虚拟机中执行的线程处于此状态;BLOCKED
- 被阻塞等待监视器锁定的线程处于此状态;WAITING
- 正在等待另一个线程执行特定动作的线程处于此状态;TIMED_WAITING
- 正在等待另一个线程执行动作到达指定等待时间的线程处于此状态;TERMINATED
- 已退出的线程处于此状态。
在程序中观察
java">public class TestState {public static void main(String[] args) throws InterruptedException {// 1. 尝试用 Lambda 表达式,创建一个线程Thread thread = new Thread(()->{// 让线程进行一段时间的休眠for (int i = 0; i < 5; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("线程休眠结束");});// 2. 线程创建后,第一次观察线程状态Thread.State state = thread.getState();System.out.println("当前线程状态:" + state); // NEW// 3. 启动线程,然后继续观察线程状态thread.start();state = thread.getState();System.out.println("当前线程状态:" + state); // RUN// 4. 只要线程没有终止,就一直输出状态while (thread.getState() != Thread.State.TERMINATED) {Thread.sleep(100);// 更新状态state = thread.getState();// 输出状态System.out.println("当前线程状态:" + state);}// 线程中断或者结束,一旦进入死亡状态,就不能再次启动
// thread.start();}}
可以观察到:
当前线程状态:NEW
当前线程状态:RUNNABLE
当前线程状态:TIMED_WAITING
当前线程状态:TIMED_WAITING
当前线程状态:TIMED_WAITING
......
当前线程状态:TIMED_WAITING
当前线程状态:TIMED_WAITING
当前线程状态:TIMED_WAITING
当前线程状态:TIMED_WAITING
当前线程状态:TIMED_WAITING
当前线程状态:TIMED_WAITING
线程休眠结束
当前线程状态:TERMINATED
线程中断或者结束,一旦进入死亡状态,就不能再次启动。
# 线程中断或者结束,一旦进入死亡状态,就不能再次启动
Exception in thread "main" java.lang.IllegalThreadStateExceptionat java.lang.Thread.start(Thread.java:708)at com.state.demo05.TestState.main(TestState.java:47)
三、线程使用方法
1. 线程停止
停止线程是一个敏感的操作,不当的停止方式可能会导致资源泄露、数据不一致等问题。因此,建议使用安全的方式来停止线程。
- 建议线程正常停止,不建议死循环;
- 不要使用
stop
或者destroy
等过时的,或者 JDK 不建议使用的方法; - 推荐的方式是使用一个标志位来通知线程停止。
例如,编写一个测试类来测试线程停止:
- 设置一个标志位;
- 设置一个公开的方法停止线程,用于转换标志位;
- 在主程序中调用该方法。
java">/*测试线程停止1. 建议线程正常停止 --> 利用次数,不建议死循环2. 建议使用标志位 --> 设置一个标志位3. 不要使用 stop 或者 destroy 等过时的,或者 JDK 不建议使用的方法*/public class TestStop implements Runnable {// 1. 设置一个标志位private boolean flag = true;// 重写 run() 方法@Overridepublic void run() {int i = 0;while (flag) {System.out.println("run...Thread->" + i++);}}// 2. 设置一个公开的方法停止线程,转换标志位public void stop() {this.flag = false;}public static void main(String[] args) {TestStop testStop = new TestStop();// 启动线程new Thread(testStop).start();for (int i = 0; i < 1000; i++) {System.out.println("Main->" + i);if (i == 900) {// 3. 调用 stop 方法(自己写的),切换标志位,让线程停止testStop.stop();System.out.println("线程该停止了");}}}
}
2. 线程休眠
在 Java 中,线程可以通过调用 Thread.sleep(long millis)
方法来让当前线程暂停执行指定的时间。这个方法会使当前线程进入非运行状态,从而让出 CPU 时间片给其他线程。
millis
: 指定当前线程休眠(阻塞)的毫秒数;InterruptedException
: 如果当前线程在休眠期间被中断,则抛出此异常;sleep
时间达到后,线程进入就绪状态;sleep
可以模拟网络延时、倒计时等;- 每一个对象都有一个锁,
sleep
不会释放锁。(涉及同步问题)
案例一:抢票
模拟网络延时可以放大问题的发生性。
在抢票案例中,如果不添加延时,由于 CPU 处理速度快,票会全部被同一个人拿走;模拟网络延时,可以更好地观察程序中存在的并发问题。
java">// 模拟网络延时:放大问题的发生性
// 如果不添加延时,由于 CPU 处理速度快,票会全部被同一个人拿走
public class TestSleep implements Runnable {// 票数private int ticketNums = 10;// 重写 run 方法@Overridepublic void run() {while (true) {if (ticketNums <= 0) {break;}// 模拟延时try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 获取当前线程的名字System.out.println(Thread.currentThread().getName() + "-->拿到了第" + (ticketNums--) + "票");}}public static void main(String[] args) {// 一份资源TestSleep ticket = new TestSleep();// 多个代理// 3 个线程,模拟角色抢票new Thread(ticket, "小明").start();new Thread(ticket, "老师").start();new Thread(ticket, "黄牛党").start();}}
案例二:模拟倒计时
从 10 开始,每秒钟打印一次倒计时。
java">// 模拟倒计时
public static void tenDown() {int num = 10;while (true) {try {// 每一秒钟打印一次倒计时Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(num--);if (num <= 0) {break;}}
}
案例三:打印当前系统时间
获取系统时间,通过延时功能每隔 1 秒打印一次系统时间。
java">// 打印当前系统时间
public static void printTime() {// 获取系统时间Date startTime = new Date(System.currentTimeMillis());// 每隔 1s 打印一次while (true) {try {// 延时Thread.sleep(1000);// 使用时间格式工厂 "HH:mm:ss",打印时间System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));//更新时间startTime = new Date(System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();}}
}
3. 线程礼让
线程礼让(Thread Yielding)是指当前线程主动放弃剩余的 CPU 时间片,让调度器重新选择其他线程来执行。
在 Java 中,可以通过 Thread.yield()
方法来实现线程礼让。
Thread.yield()
方法可以使当前线程从运行状态变为就绪状态,从而让调度器有机会选择其他就绪的线程来执行。- 这个方法并不保证当前线程一定会立即停止执行,也不保证其他线程一定会立即开始执行。它只是向调度器发出一个建议,表示当前线程愿意让出 CPU 时间片。(礼让不一定成功)
例如,启动两个线程来测试礼让:
java">public class TestYield {public static void main(String[] args) {MyYied myYied = new MyYied();new Thread(myYied, "a").start();new Thread(myYied, "b").start();}
}class MyYied implements Runnable {@Overridepublic void run() {// 获取线程名称System.out.println(Thread.currentThread().getName() + " --> start");// 线程礼让Thread.yield();System.out.println(Thread.currentThread().getName() + " --> end");}
}
运行结果:
java">// 礼让成功的例子,b 礼让给了 a
b --> start
a --> start
a --> end
b --> end// 没有礼让的情况,a 线程执行完后 b 线程才开始执行
a --> start
a --> end
b --> start
b --> end
4. 线程强制执行
Thread.join()
方法可以让当前线程等待另一个线程执行完毕,可以想象成插队。
java">// 线程强制执行,相当于插队
public class TestJoin implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("线程 vip 来啦-->" + i);}}public static void main(String[] args) throws InterruptedException {// 启动多线程TestJoin testJoin = new TestJoin();Thread thread = new Thread(testJoin);thread.start();// 主线程中执行部分for (int i = 0; i < 500; i++) {if (i == 200) {// 线程强制执行,相当于插队thread.join();}System.out.println("main-->" + i);}}
}
四、线程优先级
线程优先级是操作系统用来决定线程调度顺序的一种机制。通过设置线程的优先级,可以影响线程被调度的概率,从而优化程序的性能和响应性。
1. 优先级范围
Thread.MIN_PRIORITY
: 最低优先级,值为 1。Thread.NORM_PRIORITY
: 默认优先级,值为 5。Thread.MAX_PRIORITY
: 最高优先级,值为 10。
java">// 观察 Thread 类的源码可知:/*** The minimum priority that a thread can have.*/
public final static int MIN_PRIORITY = 1;/*** The maximum priority that a thread can have.*/public final static int MAX_PRIORITY = 10;
2. 设置 / 获取优先级
setPriority(int priority)
: 设置线程的优先级。getPriority()
: 获取线程的当前优先级。
java">// 观察 Thread 类的源码可知:public final void setPriority(int newPriority) {ThreadGroup g;checkAccess();if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {throw new IllegalArgumentException();}if((g = getThreadGroup()) != null) {if (newPriority > g.getMaxPriority()) {newPriority = g.getMaxPriority();}setPriority0(priority = newPriority);}
}public final int getPriority() {return priority;
}
3. 测试线程的优先级
编写测试程序,测试线程的优先级:
- 获取主线程的优先级,主线程默认为 5;
- 创建线程对象,设置优先级,优先级的设定建议在
start()
之前; - 优先级高的线程不一定先执行,只能说先执行的概率比较大。
java">/*优先级的设定建议在 start() 之前优先级低只是意味着获得调度的概率低并不是优先级低就不会被调用了这都是看 CPU 的调度*/public class TestPriority {public static void main(String[] args) {// 获取主线程的优先级,是默认的System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());// 创建线程对象MyPriority myPriority = new MyPriority();Thread t1 = new Thread(myPriority);Thread t2 = new Thread(myPriority);Thread t3 = new Thread(myPriority);Thread t4 = new Thread(myPriority);Thread t5 = new Thread(myPriority);Thread t6 = new Thread(myPriority);// 先设置优先级,再启动线程t1.start();t2.setPriority(1);t2.start();t3.setPriority(4);t3.start();t4.setPriority(Thread.MAX_PRIORITY); // MAX_PRIORITY == 10t4.start();}}class MyPriority implements Runnable {@Overridepublic void run() {// 获取线程名称 + 线程优先级System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());}
}
测试结果参考:
# 优先级高的线程不一定先执行
main-->5
Thread-0-->5
Thread-1-->1
Thread-2-->4
Thread-3-->10-------------------
main-->5
Thread-0-->5
Thread-3-->10
Thread-2-->4
Thread-1-->1
五、守护线程
守护线程(Daemon Thread)是一种特殊的线程,它在后台运行,通常用于执行一些辅助性的任务,如垃圾回收、日志记录、内存监控等。
- 线程分为用户线程和守护线程;
- 虚拟机必须确保用户线程执行完毕;
- 虚拟机不用等待守护线程执行完毕。
在 Java 中,可以通过 Thread
类的 setDaemon(boolean on)
方法将线程设置为守护线程。同样,可以使用 isDaemon()
方法来检查线程是否为守护线程。
1. 案例分析:星星的守护
通过以下案例测试守护线程:星星守护人类,人类只有三万天,但星星一直都在。
创建人类
人类每天都抬头仰望星空,直到…是时候说再见了。
java">// 人类
class You implements Runnable {@Overridepublic void run() {for (int i = 0; i < 36500; i++) {System.out.println("人类抬头仰望星空-->" + i);}System.out.println("是时候说再见了");}
}
创建星星类
星星一直都在。
java">// 星星
class Star implements Runnable {@Overridepublic void run() {while (true) {System.out.println("星星一直都在");}}
}
测试类
- 设置星星为守护线程,人类默认是用户线程;
- 启动守护线程;
- 启动用户线程;
java">// 测试守护线程:虚拟机不用等待守护线程执行完毕
// 星星一直都在
// 人类只有三万天
public class TestDaemon {public static void main(String[] args) {Star star = new Star();You you = new You();Thread thread = new Thread(star);// 设置星星为守护线程thread.setDaemon(true); // 默认为 false,指代用户线程// 守护线程启动thread.start();// 用户线程启动new Thread(you).start();}
}
测试结果
虚拟机不用等待守护线程执行完毕,当户线程执行完毕后,程序会停止。
程序停止需要一点时间,守护线程会继续执行一会。
...
星星一直都在
星星一直都在
星星一直都在
人类抬头仰望星空-->36490
人类抬头仰望星空-->36491
星星一直都在
星星一直都在
星星一直都在
人类抬头仰望星空-->36492
人类抬头仰望星空-->36493
人类抬头仰望星空-->36494
星星一直都在
人类抬头仰望星空-->36495
人类抬头仰望星空-->36496
人类抬头仰望星空-->36497
人类抬头仰望星空-->36498
人类抬头仰望星空-->36499
星星一直都在
星星一直都在
星星一直都在
星星一直都在
星星一直都在
是时候说再见了
星星一直都在
星星一直都在
星星一直都在
参考资料
狂神说 Java 多线程:https://www.bilibili.com/video/BV1V4411p7EF
TIOBE 编程语言走势: https://www.tiobe.com/tiobe-index/
Typora 官网:https://www.typoraio.cn/
Oracle 官网:https://www.oracle.com/
Notepad++ 下载地址:https://notepad-plus.en.softonic.com/
IDEA 官网:https://www.jetbrains.com.cn/idea/
Java 开发手册:https://developer.aliyun.com/ebook/394
Java 8 帮助文档:https://docs.oracle.com/javase/8/docs/api/
MVN 仓库:https://mvnrepository.com/