为什么要有线程及其生命周期
壹、为什么要有线程
线程(Thread)是操作系统和编程语言中实现并发执行的核心机制,其存在的根本目的是提高程序执行效率和资源利用率。以下从计算机底层原理、程序执行模式及实际场景等维度,详细解析为什么需要线程:
一、从 "单任务" 到 "多任务":线程的诞生背景
早期计算机只能单任务执行(如 DOS 系统):一个程序运行时,其他程序必须等待其结束。这种模式存在严重缺陷:
- 资源浪费:当程序进行 I/O 操作(如读取文件、网络请求)时,CPU 处于空闲状态(等待 I/O 完成),但无法处理其他任务。
- 响应缓慢:若一个任务执行时间长(如复杂计算),整个系统会陷入 "假死",用户操作无响应。
为解决这些问题,操作系统引入了进程(Process)机制,实现多任务并发。但进程存在创建销毁成本高、资源占用大、切换开销大等问题(进程包含独立的内存空间、文件描述符等)。
线程的出现:线程是进程内的轻量级执行单元,多个线程共享进程的内存空间和资源,切换成本远低于进程。线程让多任务并发更高效,成为现代编程的基础。
二、线程的核心价值:提升效率与资源利用率
1. 充分利用 CPU 资源(减少空闲时间)
现代计算机几乎都配备多核 CPU,线程能让程序并行利用多个核心,同时在单核心下通过时间片轮转实现并发,减少 CPU 空闲。
- 示例:一个程序包含两个任务:A(计算密集型,需大量 CPU)和 B(I/O 密集型,如读取文件)。
- 单线程执行:A 执行时 CPU 忙碌,B 执行时 CPU 空闲(等待 I/O),总耗时 = A 耗时 + B 耗时。
- 多线程执行:A 和 B 同时运行,CPU 在 A 执行时工作,B 等待 I/O 时 CPU 可处理 A(或其他任务),总耗时 ≈ max (A 耗时,B 耗时),效率显著提升。
2. 优化程序响应性(分离任务优先级)
线程可将程序的 "核心功能" 与 "辅助功能" 分离,避免耗时操作阻塞关键流程。
- 典型场景:图形界面(GUI)程序中,若用单线程处理用户点击(关键)和文件下载(耗时),下载时界面会冻结(无法响应用户操作)。解决方案:用主线程处理用户交互,子线程处理文件下载,确保界面始终响应。
3. 简化并发编程模型
线程比进程更轻量,且共享进程资源,大幅降低了并发编程的复杂度:
- 线程间通信无需复杂的进程间通信(IPC)机制(如管道、共享内存),可通过共享内存直接交互(需注意同步)。
- 开发中可按功能拆分线程(如网络请求线程、数据处理线程、UI 线程),逻辑更清晰。
4. 适配现代硬件与应用场景
- 多核 CPU:线程可直接映射到不同核心,实现真正的并行计算(如大数据处理、科学计算)。
- I/O 密集型应用:服务器(如 Web 服务器、数据库)需同时处理 thousands 级请求,线程池(多线程)是支撑高并发的核心机制。
- 实时系统:需快速响应外部事件(如传感器数据),线程的快速切换能力保证了实时性。
三、线程与相关概念的对比:为什么选择线程?
执行模式 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
单线程 | 简单,无并发问题 | 无法利用多核,I/O 时 CPU 空闲 | 简单工具、脚本程序 |
多进程 | 隔离性好(一个崩溃不影响其他) | 资源占用大,切换成本高,通信复杂 | 独立功能模块(如浏览器标签页) |
多线程 | 轻量,共享资源,切换快,通信简单 | 需处理线程安全问题(如竞态条件) | 大多数并发场景(服务器、GUI 等) |
四、线程的挑战与应对
线程并非完美,引入了新的问题:
- 线程安全:多个线程共享资源可能导致数据不一致(如竞态条件),需通过锁(
synchronized
、Lock
)、原子类等机制解决。 - 上下文切换:线程切换需保存 / 恢复状态(寄存器、程序计数器等),过度切换会消耗 CPU,需合理控制线程数量。
- 死锁 / 活锁:不当的同步逻辑可能导致线程无限等待,需通过规范锁顺序、设置超时等方式避免。
这些问题并非线程本身的缺陷,而是并发编程的固有挑战,通过合理设计(如使用线程池、并发容器)可有效规避。
五、总结
线程的存在是为了突破单任务执行的效率瓶颈,核心价值体现在:
- 提高 CPU 利用率:让 CPU 在 I/O 等待时不空闲,充分利用多核资源。
- 提升程序响应性:分离关键任务与耗时任务,避免阻塞。
- 简化并发模型:比进程更轻量,共享资源便于通信。
从底层硬件(多核 CPU)到上层应用(Web 服务器、GUI 程序),线程是实现高效并发的基础,是现代编程语言和操作系统不可或缺的核心机制。
贰、线程的生命周期
在 Java 中,线程的生命周期包含6 种状态,这些状态定义在Thread.State
枚举中,状态之间会根据操作发生转换。理解线程状态转换是掌握多线程编程的基础。
一、线程的 6 种状态
根据 Java 官方文档,线程的状态分为以下 6 种:
状态名称 | 说明 |
---|---|
NEW(新建) | 线程对象已创建,但未调用start() 方法(尚未启动)。 |
RUNNABLE(可运行) | 线程已启动,正在 JVM 中运行,或等待 CPU 调度(包含传统 OS 中的 “运行” 和 “就绪” 状态)。 |
BLOCKED(阻塞) | 线程等待获取 synchronized 锁(因竞争锁而被阻塞)。 |
WAITING(无限等待) | 线程无限期等待其他线程的特定操作(如notify() ),无超时时间。 |
TIMED_WAITING(计时等待) | 线程等待指定时间后自动唤醒(如sleep(1000) 、wait(1000) )。 |
TERMINATED(终止) | 线程执行完毕(run() 方法结束)或因异常终止。 |
二、状态转换流程图
[NEW] ↓ (调用start())
[RUNNABLE] ↓ (获取锁失败) ↓ (释放锁后重新竞争)
[BLOCKED] --------------------→ [RUNNABLE]↑ ↓ (调用wait()/join()等)| [WAITING]| ↓ (被notify()/中断)| → [RUNNABLE]| ↓ (调用wait(超时)/sleep(超时)等)| [TIMED_WAITING]| ↓ (超时/被notify()/中断)└----------------------------→ [RUNNABLE]↓ (run()执行完毕/异常)[TERMINATED]
三、详细转换说明
1. NEW → RUNNABLE
- 触发条件:调用线程对象的
start()
方法(不能重复调用,否则抛IllegalThreadStateException
)。 - 示例:
Thread thread = new Thread(() -> {}); System.out.println(thread.getState()); // 输出:NEW thread.start(); System.out.println(thread.getState()); // 可能输出:RUNNABLE(取决于调度)
2. RUNNABLE ↔ BLOCKED
- RUNNABLE → BLOCKED:线程尝试获取
synchronized
锁时,若锁被其他线程持有,则进入BLOCKED
状态。 - BLOCKED → RUNNABLE:持有锁的线程释放锁后,当前线程竞争到锁,重新进入
RUNNABLE
。 - 示例:
Object lock = new Object();// 线程1先获取锁 Thread t1 = new Thread(() -> {synchronized (lock) {try { Thread.sleep(1000); } // 持有锁休眠catch (InterruptedException e) {}} });// 线程2后尝试获取锁,会进入BLOCKED Thread t2 = new Thread(() -> {synchronized (lock) { // 此时t1持有锁,t2进入BLOCKEDSystem.out.println("获取锁成功");} });t1.start(); Thread.sleep(100); // 确保t1先执行 t2.start(); System.out.println(t2.getState()); // 输出:BLOCKED
3. RUNNABLE ↔ WAITING
RUNNABLE → WAITING:
- 调用
object.wait()
(必须在synchronized
块中,释放锁并等待)。 - 调用
thread.join()
(等待目标线程执行完毕)。 - 调用
LockSupport.park()
(无参数版本,阻塞当前线程)。
- 调用
WAITING → RUNNABLE:
- 其他线程调用
object.notify()
或object.notifyAll()
(唤醒后需重新竞争锁)。 - 被
join()
的目标线程执行完毕。 - 其他线程调用
LockSupport.unpark(thread)
。
- 其他线程调用
示例(
wait()
/notify()
):Object lock = new Object(); Thread t = new Thread(() -> {synchronized (lock) {try {lock.wait(); // 释放锁,进入WAITING} catch (InterruptedException e) {}} }); t.start(); Thread.sleep(100); System.out.println(t.getState()); // 输出:WAITING// 其他线程唤醒 new Thread(() -> {synchronized (lock) {lock.notify(); // 唤醒t线程} }).start();
4. RUNNABLE ↔ TIMED_WAITING
RUNNABLE → TIMED_WAITING:
- 调用
Thread.sleep(long millis)
(不释放锁,仅暂停执行)。 - 调用
object.wait(long timeout)
(释放锁,等待指定时间)。 - 调用
thread.join(long millis)
(限时等待目标线程)。 - 调用
LockSupport.parkNanos()
或LockSupport.parkUntil()
(限时阻塞)。
- 调用
TIMED_WAITING → RUNNABLE:
- 等待时间超时。
- 其他线程调用
notify()
/notifyAll()
(需重新竞争锁)。 - 线程被中断(
interrupt()
)。
示例(
sleep()
):Thread t = new Thread(() -> {try {Thread.sleep(1000); // 进入TIMED_WAITING} catch (InterruptedException e) {} }); t.start(); Thread.sleep(100); System.out.println(t.getState()); // 输出:TIMED_WAITING
5. RUNNABLE → TERMINATED
- 触发条件:
- 线程的
run()
方法正常执行完毕。 - 线程执行中抛出未捕获的异常(导致
run()
方法终止)。
- 线程的
- 示例:
Thread t = new Thread(() -> {}); t.start(); Thread.sleep(100); // 等待线程执行完毕 System.out.println(t.getState()); // 输出:TERMINATED
四、关键注意点
RUNNABLE
的特殊含义:Java 中的RUNNABLE
包含两种情况:- 线程正在 CPU 上执行(运行中)。
- 线程就绪,等待 CPU 调度(未运行但可立即执行)。这与操作系统层面的 “运行” 和 “就绪” 状态不同,是 Java 对线程状态的简化抽象。
BLOCKED
与WAITING
的区别:BLOCKED
:等待synchronized 锁(被动等待,无需主动唤醒)。WAITING
:等待其他线程的特定操作(如notify()
,需主动唤醒)。
状态不可逆:线程进入
TERMINATED
后,无法再回到其他状态(即使调用start()
也会报错)。sleep()
与wait()
的状态差异:sleep()
:线程进入TIMED_WAITING
,不释放锁。wait()
:线程进入WAITING
或TIMED_WAITING
,释放锁。
总结
线程的生命周期是多线程调度的核心,6 种状态的转换反映了线程从创建到终止的完整过程:
- 从
NEW
启动到RUNNABLE
,再根据锁竞争、等待操作进入BLOCKED
/WAITING
/TIMED_WAITING
,最终执行完毕进入TERMINATED
。
理解这些状态转换,有助于诊断线程死锁、阻塞等问题,写出更可靠的多线程程序。