线程的生命周期与数量设置
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. 多线程的应用场景
要想“降低延迟,提高吞吐量”基本上有两个方向:
- 一个方向是优化算法;
- 另一个方向是将硬件的性能发挥到极致(和并发编程息息相关)。
前者属于算法范畴,后者则是和并发编程息息相关了。计算机主要有哪些硬件主要是两类:一个是 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. 调整线程数以达到硬件资源的最大利用