当前位置: 首页 > news >正文

线程的生命周期与数量设置

1. 线程生命周期

生命周期的概念:

  • 操作系统层面:线程的“生老病死”,叫做生命周期;
  • 在 Java 中,实现并发程序的核心手段是 多线程编程。Java 的线程本质上与操作系统线程一一对应,底层由操作系统调度执行。理解线程的生命周期,要能搞懂生命周期中各个节点的状态转换机制,是掌握并发编程的基础。

1.1. 通用的线程生命周期

通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态终止状态

五模态模型介绍:

状态

描述

初始状态(New)

线程对象在编程语言层面被创建,但操作系统线程尚未创建。

可运行状态(Runnable)

线程已准备好,可以被调度分配到 CPU 上执行。

运行状态(Running)

线程实际占用 CPU 正在执行代码。

休眠状态(Blocked/Waiting/TimedWaiting)

等待某个事件或资源,无法执行。

终止状态(Terminated)

线程执行完成或异常终止,生命周期结束。

  • 注:有的语言合并了某些状态,例如 C 的 Pthreads 合并了 “初始+可运行”,Java 合并了 “可运行+运行”。

1.2. Java中线程的生命周期

Java 语言中线程共有六种状态。

但其实在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。

Java线程六种状态:

状态

描述

NEW

刚创建,尚未启动。

RUNNABLE

正在运行或等待 CPU 时间片。

BLOCKED

等待获取对象锁(synchronized)。

WAITING

无时限等待另一个线程的动作。

TIMED_WAITING

有时限等待另一个线程的动作。

TERMINATED

线程已结束执行。

1.3. Java线程的状态转换

1. NEW → RUNNABLE

NEW状态:

Java 刚创建出来的 Thread 对象就是 NEW 状态。

// --------方法1:继承 Thread 对象,重写 run() 方法-----------------
// 自定义线程对象
class MyThread extends Thread {public void run() {// 线程需要执行的代码......}
}
// 创建线程对象
MyThread myThread = new MyThread();// ----方法2:实现 Runnable 接口,重写 run() 方法,并将该实现类作为创建 Thread 对象的参数---// 实现 Runnable 接口
class Runner implements Runnable {@Overridepublic void run() {// 线程需要执行的代码......}
}
// 创建线程对象
Thread thread = new Thread(new Runner());

  • 通过 Thread.start() 方法启动线程。
  • 示例:
Thread t = new Thread(() -> { ... });
t.start();

2. RUNNABLE → BLOCKED

  • 场景:竞争 synchronized 资源失败。
  • 唤醒条件:锁释放后获得锁。

3. RUNNABLE → WAITING

  • 进入条件:
    • Object.wait()(需持有锁)
    • Thread.join()(无超时)
    • LockSupport.park()
  • 唤醒方式:
    • notify() / notifyAll()
    • join() 的目标线程结束
    • LockSupport.unpark(Thread)

4. RUNNABLE → TIMED_WAITING

  • 进入条件:
    • Thread.sleep(time)
    • Object.wait(time)
    • Thread.join(time)
    • LockSupport.parkNanos()/parkUntil()
  • 唤醒方式:
    • 时间到期
    • 被中断
  • 区别:
    • TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。

5. 任意状态 → TERMINATED

  • 正常执行完成或异常终止。
  • run() 方法执行结束。
  • 被外部中断或发生未捕获异常。

中断机制:interrupt() 与线程终止

使用:Thread.interrupt()

  • 通知线程中断,不强制终止。
  • 响应方式:

        抛出异常:在阻塞方法(如 sleep()wait())中会抛出 InterruptedException

        主动检测:通过 Thread.isInterrupted() 检测中断状态

        I/O 情况

      • 阻塞在 InterruptibleChannel:抛出 ClosedByInterruptException
      • 阻塞在 Selector:立即返回

示例:

public class InterruptDemo {public static void main(String[] args) {Thread worker = new Thread(new Task(), "WorkerThread");worker.start();// 主线程等待 3 秒后中断 worker 线程try {Thread.sleep(3000);} catch (InterruptedException e) {System.out.println("主线程被中断");}System.out.println("主线程:尝试中断子线程...");worker.interrupt();  // 发出中断信号}static class Task implements Runnable {@Overridepublic void run() {while (true) {// 检查是否收到中断请求if (Thread.currentThread().isInterrupted()) {System.out.println(Thread.currentThread().getName() + ":检测到中断,准备退出...");break;}try {System.out.println(Thread.currentThread().getName() + ":正在工作...");Thread.sleep(1000);  // 可能在这里抛出 InterruptedException} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + ":sleep 中被中断!");// 设置中断标志(因为被清除),并跳出循环Thread.currentThread().interrupt();break;}}System.out.println(Thread.currentThread().getName() + ":已退出。");}}
}

线程调试技巧:

1. jstack 命令:示例输出

  • Thread State: 当前线程状态
  • locked / waiting to lock: 显示锁信息
  • 死锁检测:明确标出死锁线程及调用栈

2. VisualVM可视化工具:可视化线程分析

  • 查看线程运行状态、CPU 占用
  • 手动导出线程 dump,辅助定位死锁或阻塞

2. 线程数量设置

  • 多线程的目标是提升 CPU 和 I/O 的综合利用率
  • 合适的线程数量应以硬件资源为基础,结合任务特性;
  • CPU 密集型任务,线程数不宜多;
  • I/O 密集型任务,线程数可以远大于 CPU 核心数;
  • 理论公式帮助我们建立模型,压测是最终依据

2.1. 使用多线程的目的

目的:

  • 提升程序性能。

性能的度量指标:

  • 延迟(Latency):从发出请求到收到响应所需的时间,越短越好。
  • 吞吐量(Throughput):单位时间内处理的请求数,越大越好。

我们使用多线程的核心目的就是:降低延迟,提高吞吐量

2.2. 多线程的应用场景

要想“降低延迟,提高吞吐量”基本上有两个方向:

  1. 一个方向是优化算法
  2. 另一个方向是将硬件的性能发挥到极致(和并发编程息息相关)

前者属于算法范畴,后者则是和并发编程息息相关了。计算机主要有哪些硬件主要是两类:一个是 I/O,一个是 CPU。简言之,在并发编程领域,提升性能本质上就是提升硬件的利用率,再具体点来说,就是提升 I/O 的利用率和 CPU 的利用率

多线程:解决 了CPU 和 I/O 设备综合利用率问题。

操作系统已经解决了磁盘和网卡的利用率问题,利用中断机制还能避免 CPU 轮询 I/O 状态,也提升了 CPU 的利用率。但是操作系统解决硬件利用率问题的对象往往是单一的硬件设备,而我们的并发程序,往往需要 CPU 和 I/O 设备相互配合工作,也就是说,我们需要解决 CPU 和 I/O 设备综合利用率的问题。关于这个综合利用率的问题,操作系统虽然没有办法完美解决,但是却给我们提供了方案,那就是:多线程。

应用示例:

单线程场景(CPU 和 I/O 交替):

  • CPU 运算时,I/O 空闲
  • I/O 等待时,CPU 空闲
    各自利用率只有 50%

多线程场景(2 个线程交替):

  • A线程做CPU运算,B线程进行I/O
  • A线程做I/O,B线程做CPU运算
    CPU 和 I/O 利用率都达到 100%,吞吐量提升一倍

多核 CPU 下的多线程优势:

在单核时代,多线程主要就是用来平衡 CPU 和 I/O 设备的。如果程序只有 CPU 计算,而没有 I/O 操作的话,多线程不但不会提升性能,还会使性能变得更差,原因是增加了线程切换的成本。但是在多核时代,这种纯计算型的程序也可以利用多线程来提升性能。

例如:使用 4 核 CPU 计算 1+2+...+100 亿

  • 单线程:CPU 利用率只有 25%
  • 4 线程:每个线程一个核,CPU 利用率 100%,响应时间缩短至 25%

2.3. 线程数量设置策略

我们的程序一般都是 CPU 计算和 I/O 操作交叉执行的,由于 I/O 设备的速度相对于 CPU 来说都很慢,所以大部分情况下,I/O 操作执行的时间相对于 CPU 计算来说都非常长,这种场景我们一般都称为 I/O 密集型计算;和 I/O 密集型计算相对的就是 CPU 密集型计算了,CPU 密集型计算大部分场景下都是纯 CPU 计算

I/O 密集型程序和 CPU 密集型程序,计算最佳线程数的方法是不同的。

1. 对于CPU密集型计算场景:

线程数量 = CPU 核数 + 1

对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的。不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。

2. 对于 I/O 密集型的计算场景:

最佳线程数 = CPU 核数 × [1 + (I/O 耗时 / CPU 耗时)]

如果 CPU 计算和 I/O 操作的耗时是 1:2,那多少个线程合适呢?是 3 个线程,如下图所示:CPU 在 A、B、C 三个线程之间切换,对于线程 A,当 CPU 从 B、C 切换回来时,线程 A 正好执行完 I/O 操作。这样 CPU 和 I/O 设备的利用率都达到了 100%。

工程实践建议:

1. 估算 I/O 与 CPU 的耗时比值,用于初步设定线程数;

2. 进行性能压测,关注:

  • CPU 利用率
  • I/O 利用率
  • 吞吐量和响应时间

3. 调整线程数以达到硬件资源的最大利用

相关文章:

  • 【TCP/IP和OSI模型以及区别——理论汇总】
  • 【工具使用】STM32CubeMX-FreeRTOS操作系统-任务、延时、定时器篇
  • DINO-R1
  • C语言-指针基础概念
  • leetcode题解236:二叉树的最近公共祖先
  • Elasticsearch中什么是分析器(Analyzer)?它由哪些组件组成?
  • JS利用原型链实现继承
  • 【leetcode】9. 回文数
  • (每日一道算法题)求根节点到叶节点数字之和
  • Java-IO流之字符输出流详解
  • qiankun模式下 主应用严格模式,子应用el-popover 点击无效不显示
  • GAN训练困境与模型分类:损失值异常与生成判别模型差异解析
  • 第八部分:第六节 - 状态管理 (基础):协调多个界面的状态
  • 基于 ShardingSphere + Seata 的最终一致性事务完整示例实现
  • 局部变量-线程安全
  • 深度学习项目之RT-DETR训练自己数据集
  • 【docker】容器技术如何改变软件开发与部署格局
  • CMake在VS中使用远程调试
  • PocketFlow 快速入门指南
  • 【仿生】硬件缺失,与组装调试,皮肤问题
  • 珠宝网站源码免费下载/竞价托管公司排名
  • 湘潭做网站价格优选磐石网络/优化关键词推广
  • 自己怎样做网站显示危险/2020做seo还有出路吗
  • 网站开发公司市场/疫情最严重的三个省
  • 设计网站建设常州/腾讯广告投放平台官网
  • 网站运营技巧/河南网站建设报价