0 %

多线程 04:线程状态,线程的五大基本状态及状态转换,以及线程使用方法、优先级、守护线程的相关知识

2025-10-26 21:09:49

一、概述

记录时间 [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 - 已退出的线程处于此状态。

在程序中观察

创建一个线程,线程创建后,第一次观察线程状态;启动线程,然后继续观察线程状态;一直观察线程状态,直到线程停止。

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;线程停止后,观察线程状态为 TERMINATED。

当前线程状态: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.IllegalThreadStateException

at java.lang.Thread.start(Thread.java:708)

at com.state.demo05.TestState.main(TestState.java:47)

三、线程使用方法

1. 线程停止

停止线程是一个敏感的操作,不当的停止方式可能会导致资源泄露、数据不一致等问题。因此,建议使用安全的方式来停止线程。

建议线程正常停止,不建议死循环;不要使用 stop 或者 destroy 等过时的,或者 JDK 不建议使用的方法;推荐的方式是使用一个标志位来通知线程停止。

例如,编写一个测试类来测试线程停止:

设置一个标志位;设置一个公开的方法停止线程,用于转换标志位;在主程序中调用该方法。

/*

测试线程停止

1. 建议线程正常停止 --> 利用次数,不建议死循环

2. 建议使用标志位 --> 设置一个标志位

3. 不要使用 stop 或者 destroy 等过时的,或者 JDK 不建议使用的方法

*/

public class TestStop implements Runnable {

// 1. 设置一个标志位

private boolean flag = true;

// 重写 run() 方法

@Override

public 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 处理速度快,票会全部被同一个人拿走;模拟网络延时,可以更好地观察程序中存在的并发问题。

// 模拟网络延时:放大问题的发生性

// 如果不添加延时,由于 CPU 处理速度快,票会全部被同一个人拿走

public class TestSleep implements Runnable {

// 票数

private int ticketNums = 10;

// 重写 run 方法

@Override

public 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 开始,每秒钟打印一次倒计时。

// 模拟倒计时

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 秒打印一次系统时间。

// 打印当前系统时间

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 时间片。(礼让不一定成功)

例如,启动两个线程来测试礼让:

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 {

@Override

public void run() {

// 获取线程名称

System.out.println(Thread.currentThread().getName() + " --> start");

// 线程礼让

Thread.yield();

System.out.println(Thread.currentThread().getName() + " --> end");

}

}

运行结果:

// 礼让成功的例子,b 礼让给了 a

b --> start

a --> start

a --> end

b --> end

// 没有礼让的情况,a 线程执行完后 b 线程才开始执行

a --> start

a --> end

b --> start

b --> end

4. 线程强制执行

Thread.join() 方法可以让当前线程等待另一个线程执行完毕,可以想象成插队。

// 线程强制执行,相当于插队

public class TestJoin implements Runnable {

@Override

public 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。

// 观察 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(): 获取线程的当前优先级。

// 观察 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() 之前;优先级高的线程不一定先执行,只能说先执行的概率比较大。

/*

优先级的设定建议在 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 == 10

t4.start();

}

}

class MyPriority implements Runnable {

@Override

public 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. 案例分析:星星的守护

通过以下案例测试守护线程:星星守护人类,人类只有三万天,但星星一直都在。

创建人类

人类每天都抬头仰望星空,直到…是时候说再见了。

// 人类

class You implements Runnable {

@Override

public void run() {

for (int i = 0; i < 36500; i++) {

System.out.println("人类抬头仰望星空-->" + i);

}

System.out.println("是时候说再见了");

}

}

创建星星类

星星一直都在。

// 星星

class Star implements Runnable {

@Override

public void run() {

while (true) {

System.out.println("星星一直都在");

}

}

}

测试类

设置星星为守护线程,人类默认是用户线程;启动守护线程;启动用户线程;

// 测试守护线程:虚拟机不用等待守护线程执行完毕

// 星星一直都在

// 人类只有三万天

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/

Posted in 世界杯小组赛分组
Copyright © 2088 2034年世界杯_足球中国世界杯预选赛 - qdhuaxue.com All Rights Reserved.
友情链接