Java并发编程实战 03 | Java线程状态

在本文中,我们将深入探讨 Java 线程的六种状态以及它们之间的转换过程。其实线程状态之间的转换就如同生物生命从诞生、成长到最终死亡的过程一样。也是一个完整的生命周期。

首先我们来看看操作系统中线程的生命周期是如何转换的。

操作系统中的线程状态转换

线程在操作系统中通常有五种状态。

在现代操作系统中,线程被视为轻量级进程,因此操作系统中的线程状态实际上与进程状态类似。

从实际意义上讲,除了 new 和 terminated 状态外,线程主要有以下三种状态:

  • 就绪 (Ready) :线程已准备好执行,但可能由于调度策略或其他因素未能获得 CPU ,处于就绪状态的线程只要获得CPU,它就会进入运行状态。
  • 正在运行(RUNNING):线程当前正在占用CPU,执行其任务。
  • 等待(WAITING):线程正在等待某些事件的发生或资源的获取(例如I/O操作)。

new 和 terminated 状态在线程的实际运行过程中并不频繁涉及,因此讨论这两个状态在实际应用中并不具有太大意义。

Java 线程的 6 种状态

在 Java 中,线程状态的定义与操作系统中的状态并不完全相同。Java 的线程状态提供了更细粒度的管理。

Java 线程状态通过 java.lang.Thread.State 枚举进行定义:

public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}

这些状态之间的转换关系如下图所示:

接下来我们就对Java线程的六种状态进行深入分析。

1. NEW

线程对象被创建出来但是start() 方法还没有被调用,这个时候线程处于new状态。

public class ThreadStateDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {});System.out.println(thread.getState());}
}//输出:
NEW

处于new状态的线程可以扭转到RUNNABLE状态。

2. RUNNABLE

处于new状态的线程,通过调用Thread实例的start()方法可以使线程进入RUNNABLE状态。

注意,Java线程中的RUNNABLE状态对应的是操作系统中线程定义的两种 状态:

  1. 就绪 (Ready)
  2. 正在运行(RUNNING)

也就是说在Java中,当一个线程正在运行时,如果CPU时间片用完了, CPU 被调度去执行其他任务,导致该线程暂时停止运行,它的状态仍保持为 RUNNABLE。因为该线程随时可能被重新调度回 CPU 上继续执行。

一个简单的线程示例:

public class ThreadExample {public static void main(String[] args) {// 创建一个线程对象,但尚未启动Thread myThread = new Thread(() -> {System.out.println("线程正在运行...");});// 打印线程状态,是 NEWSystem.out.println("线程状态: " + myThread.getState());// 启动线程myThread.start();// 打印线程状态,是 RUNNABLE(取决于线程调度)System.out.println("线程状态: " + myThread.getState());}
}

前面讲了处于NEW状态的线程,通过调用Thread实例的start()方法进入RUNNABLE状态。关于start()方法,其实有两个值得思考的问题:

  1. 是否可以在同一个线程对象上重复调用 start() 方法?
  2. 如果一个线程已经执行完毕,处于 TERMINATED 状态,是否可以再次调用 start() 方法?

要回答这两个问题,我们可以查看 start() 方法的源码。

public synchronized void start() {if (threadStatus != 0)throw new IllegalThreadStateException();group.add(this);boolean started = false;try {start0();started = true;} finally {try {if (!started) {group.threadStartFailed(this);}} catch (Throwable ignore) {}}
}

在 start() 方法中,可以看到一个名为 threadStatus 的变量。如果这个变量不等于 0,再次调用 start() 方法时,就会直接抛出 IllegalThreadStateException 异常。

接着,start() 方法调用了一个名为 start0() 的方法,该方法是一个本地方法(native method),由底层操作系统或虚拟机实现,因此我们无法从 Java 代码中看到它对 threadStatus 的具体处理方式。不过,这并不妨碍我们了解其行为。

我们可以通过在调用 start() 方法时打印出当前线程的状态,然后尝试多次调用 start() 方法,以观察并理解 IllegalThreadStateException 异常的触发条件和线程状态的变化。

public class ThreadStateDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {});System.out.println(thread.getState());//第一次调用thread.start(); System.out.println(thread.getState());//第二次调用thread.start(); }
}//输出:
NEW
RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateExceptionat java.lang.Thread.start(Thread.java:708)at thread.basic.ThreadStateDemo.main(ThreadStateDemo.java:11)

可以看到,第一次调用 start() 方法是没有问题的,但在第二次调用时会报错。错误信息显示在 java.lang.Thread.start(Thread.java:708) 处,这个错误的原因是线程的状态检查失败。

这是因为当线程的 start() 方法第一次被调用时,线程被正确启动并进入 RUNNABLE 状态。第二次再调用start()时,由于线程的状态不再是初始NEW状态(0),直接抛出异常。

最后总结一下:

  1. 如果尝试在同一个线程上重复调用 start() 方法,会抛出 IllegalThreadStateException 异常,也就是说同一个线程对象只能启动一次。
  2. 如果线程已经完成(处于 TERMINATED 状态),再次调用 start() 方法也是不允许的,会同样抛出 IllegalThreadStateException 异常。线程一旦结束就不能再被重新启动。

处于RUNNABLE状态的线程根据不同的条件可以扭转到BLOCKED,WAITING,TIMED_WAITING,TERMINATED等状态。

3. BLOCKED

当线程处于 BLOCKED 状态时,表示它正在等待获取一个锁,以便进入同步区域。

我们可以用一个生活中的例子来说明 BLOCKED 状态:假设你去银行办理业务,当你走到某个窗口时,发现已经有一个人在你前面,此时你必须等到前面的人办完业务并离开窗口后,才能开始办理你的业务。

在这个例子中,你就是线程 B,前面的人是线程 A。当 A 正在占用窗口(即锁),而 B 需要等待 A 完成并释放窗口资源,这期间线程 B 就处于 BLOCKED 状态。

针对这个例子,写了一个简单的代码,如下:

public class BlockCase {private synchronized void businessProcessing() {try {System.out.println("Thread[" + Thread.currentThread().getName() + "] performs business processing");Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {BlockCase blockCase = new BlockCase();Thread A = new Thread(blockCase::businessProcessing, "A");Thread B = new Thread(blockCase::businessProcessing, "B");A.start();B.start();System.out.println("Thread[" + A.getName() + "] state:" + A.getState());System.out.println("Thread[" + B.getName() + "] state:" + B.getState());}
}

在这个例子中,我们使用 Thread.sleep() 方法来模拟业务处理所需的时间。

你可能会觉得线程 A 会首先调用同步方法,而在同步方法内调用 Thread.sleep() 方法,使其进入 TIMED_WAITING 状态;与此同时,线程 B 则在等待线程 A 释放锁,因此它的状态会是 BLOCKED。然而,实际情况并不总是如此!这是因为:

  1. 除了线程 A 和线程 B,程序中还有一个主线程在运行。
  2. 当我们调用 start() 方法启动线程时,线程从调用 start() 到真正开始执行 run() 方法之间存在一定的时间差。在这个时间差内,CPU 的调度竞争结果会导致不同的输出。

下面是一种可能的输出:

//输出:
Thread[A] performs business processing
Thread[A] state:RUNNABLE
Thread[B] state:BLOCKED
Thread[B] performs business processing

这种场景下,线程 A 正在执行 businessProcessing 方法,线程 B 正在等待获取 businessProcessing 方法的锁,因此它处于 BLOCKED 状态。

如果你希望线程 A 打印出 TIMED_WAITING 状态,而线程 B 打印出 BLOCKED 状态,可以稍微修改主线程的逻辑。在调用 A.start() 后,让主线程“休息一会儿”,使用 Thread.sleep() 方法让线程 A 有时间去获取锁。

注意的是,主线程的休眠时间应该足够长,确保线程 A 正在执行并进入 TIMED_WAITING 状态,但又不应太长,以免线程 A 完成任务并释放锁。这样,在线程 A 执行期间,线程 B 仍然会尝试获取锁并进入 BLOCKED 状态。

这样一来,我们可以有效地控制两个线程的状态,使得线程 A 进入 TIMED_WAITING 状态,而线程 B 则处于 BLOCKED 状态。

public static void main(String[] args) throws InterruptedException {BlockCase blockCase = new BlockCase();Thread A = new Thread(blockCase::businessProcessing, "A");Thread B = new Thread(blockCase::businessProcessing, "B");// Fixed output TIMED_WAITING state and BLOCKED stateA.start();Thread.sleep(1000); //Sleep time should be less than business processing timeB.start();System.out.println("Thread[" + A.getName() + "] state:" + A.getState());Sy  stem.out.println("Thread[" + B.getName() + "] state:" + B.getState());}//输出:
Thread[A] performs business processing
Thread[A] state:TIMED_WAITING
Thread[B] state:BLOCKED
Thread[B] performs business processing 

在这个例子中,两个线程的状态会按照以下步骤转换:

线程 A 的状态转换过程:

  1. NEW: 线程 A 被创建,但还未启动。
  2. RUNNABLE: 调用 A.start() 后,线程 A 进入可运行状态,等待被 CPU 调度。
  3. TIMED_WAITING: 线程 A 获取到锁后,调用 Thread.sleep() 方法进入计时等待状态。
  4. RUNNABLE: 等待时间结束,线程 A 重新进入可运行状态。
  5. TERMINATED: 线程 A 执行完任务,进入终止状态。

线程 B 的状态转换过程:

  1. NEW: 线程 B 被创建,但还未启动。
  2. RUNNABLE: 调用 B.start() 后,线程 B 进入可运行状态,等待被 CPU 调度。
  3. BLOCKED: 线程 B 尝试获取锁失败,因为线程 A 已经持有锁,所以 B 进入阻塞状态。
  4. RUNNABLE: 线程 A 释放锁后,线程 B 获取到锁,进入可运行状态。
  5. TIMED_WAITING: 线程 B 进入临时等待状态(例如,通过 Thread.sleep())。
  6. RUNNABLE: 等待时间结束,线程 B 重新进入可运行状态。
  7. TERMINATED: 线程 B 执行完任务,进入终止状态。

处于BLOCKED状态的线程获取到锁后可以扭转到RUNNABLE状态。

4. WAITING

线程进入WAITING状态的方式有三种:

  1. Object.wait():将当前线程置于等待状态,直到另一个线程调用同一对象的 notify() 或 notifyAll() 方法来唤醒它。
  2. Thread.join():使当前线程等待指定的线程执行完毕后再继续运行。底层实现是调用 Object.wait() 方法。
  3. LockSupport.park():使当前线程进入等待状态,直到被显式地唤醒。它的控制权完全取决于是否获得了唤醒权限。

让我们继续用之前的银行办理业务的例子来解释 WAITING 状态。

假设你在银行办理业务时,终于轮到你到柜台办理了。但是,不幸的是,柜台的电脑突然坏了。为了完成业务,你必须等待维修人员修好电脑后才能继续办理。

在这个场景中,假设你是线程 A,维修人员是线程 B。尽管你已经在柜台前等待(即获得了锁),但是你还要释放锁,此时线程A的状态是WAITING,然后线程B获得锁,进入RUNNABLE状态。

如果线程 B 不主动唤醒线程 A(通过调用 notify() 或 notifyAll() 方法),线程 A 将会一直处于等待状态,无法继续执行。

以下是一个简单的代码示例,演示了如何使用 Object.wait() 和 notify() 方法来实现这种行为:

public class WaitingCase {private synchronized void businessProcessing() {try {System.out.println("Thread[" + Thread.currentThread().getName() + "] expects to process business, but the computer is broken");// Release the monitor(lock)wait();// business processingSystem.out.println("Thread[" + Thread.currentThread().getName() + "] continues to process business");Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}}private synchronized void repairComputer() {System.out.println("Thread[" + Thread.currentThread().getName() + "] comes to repair the computer");try {// Simulated RepairThread.sleep(1000L);System.out.println("Thread[" + Thread.currentThread().getName() + "] has completed the repair.");notify();} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) throws InterruptedException {WaitingCase blockedCase = new WaitingCase();Thread A = new Thread(blockedCase::businessProcessing, "A");Thread B = new Thread(blockedCase::repairComputer, "B");A.start();Thread.sleep(500); //Used to ensure that thread A grabs the lock first. Sleep time should be less than repair timeB.start();System.out.println("Thread[" + A.getName() + "] state:" + A.getState());System.out.println("Thread[" + B.getName() + "] state:" + B.getState());}
}//输出:
Thread[A] expects to process business, but the computer is broken
Thread[B] comes to repair the computer
Thread[A] state:WAITING
Thread[B] state:TIMED_WAITING
Thread[B] has completed the repair.
Thread[A] continues to process business

关于 wait() 方法,有几个关键点需要特别强调:

  1. 持有锁:在调用 wait() 方法之前,线程必须先获得对象的监视器(锁)。换句话说,调用 wait() 的线程必须在同步代码块或同步方法中运行,即持有对象的锁。

  2. 释放锁:当线程调用 wait() 方法时,它会释放当前持有的锁,并进入等待状态。线程将保持在等待状态,直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。

  3. notify() 方法:调用 notify() 方法只能唤醒一个正在等待该锁的线程。如果有多个线程在等待同一个对象的锁,notify() 方法只会唤醒其中一个线程,这个线程并不是固定的,具体哪个线程被唤醒取决于线程调度的具体实现。

  4. notifyAll() 方法:调用 notifyAll() 方法会唤醒所有正在等待该锁的线程。这些被唤醒的线程会竞争重新获得锁,但并不保证它们会立即得到 CPU 时间片,具体的调度顺序取决于操作系统的线程调度策略。

我们再来看看Thread.join()方法。

join() 方法用于使调用线程暂停执行,直到被调用的线程执行完毕。调用 join() 的线程将进入 WAITING 状态,直到目标线程完成执行。这个方法常用于主线程中,确保在继续执行之前等待其他线程完成。

我们来回顾一下之前的 BlockCase 示例,其中 A.start() 和 B.start() 都是在主线程中直接调用的。这就像是让多个线程竞争窗口的使用权。如果参与竞争的线程越来越多,窗口就会变得非常拥挤。

为了改善这个问题,银行引入了一个新的办法:给每个办理业务的客户一个编号,按编号叫号。只有被叫到的客户才能到窗口办理业务,其余的客户则可以在休息区等待。

现在,我们可以在之前的 BlockCase 示例中扩展这个想法,假设我们有三个线程来模拟这个场景。每个线程代表一个客户,我们将使用 join() 方法来确保主线程等待所有客户(线程)完成业务后才继续执行。

public class JoinCase {private synchronized void businessProcessing() {try {System.out.println("Thread[" + Thread.currentThread().getName() + "] performs business processing");Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) throws InterruptedException {JoinCase blockedCase = new JoinCase();Thread A = new Thread(blockedCase::businessProcessing, "A");Thread B = new Thread(blockedCase::businessProcessing, "B");Thread C = new Thread(blockedCase::businessProcessing, "C");System.out.println("Please ask thread A to go to the window to handle the business.");A.start();A.join();System.out.println("Please ask thread B to go to the window to handle the business.");B.start();B.join();System.out.println("Please ask thread C to go to the window to handle the business.");C.start();}
}//输出:
Please ask thread A to go to the window to handle the business.
Thread[A] performs business processing
Please ask thread B to go to the window to handle the business.
Thread[B] performs business processing
Please ask thread C to go to the window to handle the business.
Thread[C] performs business processing

您可以尝试多次执行该程序,并且总是会得到相同的结果。

处于WAITING状态的线程被其他线程唤醒可以扭转到RUNNABLE状态。

5.TIMED_WAITING

超时等待状态 (TIMED_WAITING) 是线程在指定时间内等待的状态,时间到后线程会自动唤醒。以下方法可以使线程进入超时等待状态:

  • Thread.sleep(long millis):使当前线程休眠指定的时间,并不会释放锁。这种方法使线程进入超时等待状态,但在此期间线程持有锁。
  • Object.wait(long timeout):使线程等待指定的时间,即使没有其他线程通过 notify() 或 notifyAll() 唤醒它,也会在超时时自动唤醒。
  • Thread.join(long millis):使当前线程等待指定线程最多 millis 毫秒,如果 millis 为 0,则一直等待,直到目标线程结束。
  • LockSupport.parkNanos(long nanos):禁止当前线程在指定时间内进行线程调度,除非获得调用权限。
  • LockSupport.parkUntil(long deadline):与 parkNanos() 类似,但使用绝对时间戳作为参数。

处于TIMED_WAITING状态的线程被其他线程唤醒或等待的时间到了以后被扭转到RUNNABLE状态。

6. TERMINATED


当线程已完成执行时,处于TERMINATED状态。

已经终止的线程无法再扭转到其它状态

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/417866.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32F4按键状态机--单击、双击、长按

STM32F4按键状态机--单击、双击、长按 一、状态机的三要素二、使用状态机原因2.1资源占用方面2.2 执行效率方面:2.3 按键抖动方面: 三、状态机实现3.1 状态机分析3.1 程序实现 百度解析的状态机概念如下 状态机由状态寄存器和组合逻辑电路构成&#xff0…

深度学习 --- VGG16能让某个指定的feature map激活值最大化图片的可视化(JupyterNotebook实战)

VGG16能让某个指定的feature map激活值最大化图片的可视化 在前面的文章中,我用jupyter notebook分别实现了,预训练好的VGG16模型各层filter权重的可视化和给VGG16输入了一张图像,可视化VGG16各层的feature map。深度学习 --- VGG16卷积核的可…

Python 优雅编程:会报恩的代码(五)

文章目录 引言从文本搜索指定单词,不区分单词的大小写使用 str.lower()使用 re 模块 从文本搜索多个单词,依旧不区分单词的大小写使用 str.lower() 和循环使用 re 模块 反复执行 re.compile,re 是否会缓存编译结果?结语 引言 在 …

day47——面向对象特征之继承

一、继承(inhert) 面向对象三大特征:封装、继承、多态 继承:所谓继承,是类与类之间的关系。就是基于一个已有的类,来创建出一个新类的过程叫做继承。主要提高代码的复用性。 1.1 继承的作用 1> 实现…

【一嗨租车-注册安全分析报告-滑动验证加载不正常导致安全隐患】

前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞…

UE4_后期处理_后期处理材质及后期处理体积三—遮挡物体描边显示

一、效果: 在很多游戏中为了玩家能看到墙面背后是否有敌人,会给被遮挡的敌人增加描边显示,效果如下: 参考: https://zhuanlan.zhihu.com/p/81310476 https://zhuanlan.zhihu.com/p/358140547 二、所需知识 知识点…

Java笔试面试题AI答之JDBC(3)

文章目录 13. 编写JDBC连Oracle的程序?14. 简述JDBC的主要组件有哪些 ?15. JDBC中如何防止SQL注入攻击?1. 使用预处理语句(PreparedStatement)2. 避免在SQL查询中直接拼接用户输入的数据总结 16. JDBC的脏读是什么?哪…

Spring01——Spring简介、Spring Framework架构、Spring核心概念、IOC入门案例、DI入门案例

为什么要学 spring技术是JavaEE开发必备技能,企业开发技术选型命中率>90%专业角度 简化开发:降低企业开发的复杂度框架整合:高效整合其他技术,提高开发与运行效率 学什么 简化开发 IOCAOP 事务处理 框架整合 MyBatis 怎…

深度学习的基础_多层感知机的手动实现

多层感知机(Multilayer Perceptron,简称MLP)是一种前馈人工神经网络。它包含至少三层节点:一个输入层、一个或多个隐藏层以及一个输出层。除输入节点外,每个节点都是一个带有非线性激活函数的神经元(或称为…

Word快速重复上一步操作的三种高效方法

在日常工作、学习和生活中,我们经常需要执行一系列重复性的操作。这些操作可能简单如复制粘贴、调整图片大小,也可能复杂如编辑文档、处理数据等。为了提高效率,掌握快速重复上一步操作的方法显得尤为重要。本文将介绍三种高效的方法&#xf…

给力!Python配置文件,这一篇就够了!

在开发过程中,我们常常会用到一些固定参数或者是常量。对于这些较为固定且常用到的部分,往往会将其写到一个固定文件中,避免在不同的模块代码中重复出现从而保持核心代码整洁。 这里插播一条粉丝福利,如果你在学习Python或者有计划…

【C题成品论文已出】24数学建模国赛C题成品论文(附参考代码)免费分享

24高教社杯数学建模国赛C题成品论文 一、问题一模型建立与求解 1.1模型建立 (1)决策变量设计 表示一个26158的矩阵,其中26是平旱地梯田和山坡地的总数,15是在这几类土地上可以种植的农作物数量,8则表示从2023到203…

KCP实现原理探析

KCP 是一个轻量级的、高效的、面向 UDP 的传输协议库,专为需要低延迟和高可靠性的实时应用设计。本文针对 KCP 的主要机制和实现与原理进行分析。 1. 术语 术语 全称 说明 TCP Transmission Control Protocol 传输控制协议 RTT Round Trip Time 往返时延 …

【鸿蒙HarmonyOS NEXT】调用后台接口及List组件渲染

【鸿蒙HarmonyOS NEXT】调用后台接口及List组件渲染 一、环境说明二、调用后台接口及List组件渲染三、总结 一、环境说明 DevEco Studio 版本: API版本:以12为主 二、调用后台接口及List组件渲染 后台接口及返回数据分析 JSON数据格式如下&#xf…

Git创建项目

方法一 1.在gitee中新建仓库demo01,并勾选开源许可证,完成后gitee上面的项目demo01里只包含一个LICENSE文件 2.直接在本地电脑中新建项目文件夹demo01,双击进入这个文件夹,右键Git bash here,输入 git clone https:…

跨域问题(CORS)

文章目录 介绍解决一、添加跨域头,允许跨域1.后端配置CORS策略(4种方法)2.配置nginx 二、代理 介绍 跨域资源共享(CORS, Cross-Origin Resource Sharing)是浏览器的一个安全机制,用来防止来自一个域的网页对另一个域下的资源进行…

Linux操作系统在虚拟机VM上的安装【CentOS版本】

目录 准备工作 "CPU虚拟化"的方法 VMware的安装 Linux镜像文件的下载 开始安装 声明 新建虚拟机 安装CentOS7.6 配置Linux(CentOS7.6)操作系统 配置分区【学习者可以直接点击自动配置分区,不过还是建议学习一下手动分区】 分区原则 添加分区 …

提示工程颠覆:DSPy 引领全新范式革命

几个月前,我清楚地记得,Prompt Engineering 还是热门话题。就业市场上充斥着提示工程师的岗位,仿佛这是未来的必备技能。 然而,现在情况已经大不相同了。提示工程并不是一门艺术或科学,更像是“聪明的汉斯”现象——人类为系统提供了必要的背景,以便系统能更好地作出回应…

Maven聚合与继承

聚合 当我们一次想要构建多个项目时,而不是到每一个模块的目录下分别执行mvn命令。这个时候就需要使用到maven的聚合特性 这里第一个特殊的地方是packaging,值设置为pom。我们正常开发的其他模块中都没有声明packaging,默认使用了默认值jar&a…

【Qt】仿照qq界面的设计

widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDebug>//QT中信息调试类&#xff0c;用于输出数据&#xff0c;无需使用该类的实例化对象&#xff0c;直接使用成员函数即可 #include <QIcon>//图标类 #include <QPushButton&…