多线程入门到精通系列: 从操作系统到 Java 线程模型
目录
一、进程与线程:操作系统视角的本质区别
1.1 进程:资源分配的基本单位
1.2 线程:CPU 调度的基本单位
二、线程调度:操作系统如何管理线程
2.1 时间片轮转调度
2.2 上下文切换的代价
三、Java 线程模型:JVM 如何映射操作系统线程
3.1 1:1 线程模型
3.2 为什么 Java 不采用用户态线程?
四、Java 线程的状态:从源码看线程生命周期
五、实战:用工具观察线程状态
5.1 代码示例:创建不同状态的线程
5.2 用 jstack 查看线程状态
六、新手常见误区与最佳实践
6.1 误区 1:线程越多,程序越快
6.2 误区 2:直接调用 run () 方法启动线程
6.3 误区 3:用 stop () 方法终止线程
七、总结与下一篇预告
一、进程与线程:操作系统视角的本质区别
要理解线程,必须先搞懂进程。无论是 Windows、Linux 还是 macOS,现代操作系统的核心调度单位都是进程和线程,它们的设计源于一个朴素的需求:让计算机同时 "做" 多件事。
1.1 进程:资源分配的基本单位
进程(Process)是程序的一次执行过程。当你双击打开浏览器,操作系统就会创建一个进程:
- 它会占用一块独立的内存空间(代码、数据、堆等)
- 会申请文件句柄、网络端口等系统资源
- 拥有自己的程序计数器(记录下一条要执行的指令)
比如打开macos的程序监控,可以看到管理打开软件的基本单位是进程。
进程的关键特性是资源隔离:一个进程崩溃通常不会影响其他进程(比如浏览器崩溃不会导致微信关闭)。但这种隔离是有代价的 —— 创建进程、切换进程的开销很大(涉及内存映射、资源分配等)。
1.2 线程:CPU 调度的基本单位
线程(Thread)是进程内的执行单元,它共享进程的资源(内存、文件句柄等),但有自己的栈和程序计数器。
为什么需要线程?举个例子:当你用浏览器下载文件时,还能同时滚动页面 —— 这就是多线程的作用。如果用多进程实现,不仅资源占用大,进程间通信也很麻烦。
进程与线程的核心区别:
进程是资源分配的单位,线程是 CPU 调度的单位
进程间资源隔离,线程间资源共享
进程创建 / 切换成本高,线程创建 / 切换成本低(约为进程的 1/10 到 1/100)
二、线程调度:操作系统如何管理线程
线程创建后,由操作系统的调度器(Scheduler)负责分配 CPU 时间。
2.1 时间片轮转调度
现代操作系统基本都采用时间片轮转(Round-Robin)调度策略:
每个线程被分配一个时间片(通常 10-100 毫秒)
时间片用完后,CPU 切换到下一个线程(上下文切换)
所有线程轮流使用 CPU,宏观上看起来 "同时执行"
2.2 上下文切换的代价
当 CPU 从一个线程切换到另一个线程时,需要保存当前线程的状态(寄存器、程序计数器等),并加载新线程的状态 —— 这个过程称为上下文切换(Context Switch)。
上下文切换的代价往往被低估:
一次切换大约消耗 1-10 微秒(具体取决于硬件)
如果线程数过多,CPU 会把大部分时间花在切换上,而非执行任务
三、Java 线程模型:JVM 如何映射操作系统线程
Java 线程(java.lang.Thread
)并不是凭空存在的,它依赖于底层操作系统的线程实现。
3.1 1:1 线程模型
目前主流的 JVM(如 HotSpot)都采用1:1 线程模型:
一个 Java 线程对应一个操作系统线程(OS Thread)
JVM 负责将 Java 线程的操作(如 start、sleep)映射到 OS 线程的系统调用
线程调度完全由操作系统负责,JVM 无法干预
这种模型的优势是实现简单,能充分利用操作系统的调度能力;缺点是创建线程的成本较高(受限于 OS 线程的创建成本)。
3.2 为什么 Java 不采用用户态线程?
有些语言(如 Go)采用用户态线程(如 Goroutine),由语言 runtime 而非操作系统调度,能创建百万级线程。Java 早期也尝试过(如 JDK 1.1 的 Green Threads),但最终放弃,主要原因是:
-
操作系统调度更成熟:现代 OS 的线程调度已经过几十年优化,能充分利用多核 CPU
-
兼容原生库:很多 Java 库(如数据库驱动、网络库)依赖操作系统的阻塞 IO,用户态线程难以适配
-
开发复杂度高:实现高效的用户态线程调度器非常复杂
不过 Java 正在迎头赶上 ——Java 21 引入的虚拟线程(Virtual Threads)就是一种轻量级用户态线程,后面会专门讲解。
四、Java 线程的状态:从源码看线程生命周期
Java 线程的状态定义在Thread.State
枚举中,共 6 种状态。理解这些状态,是排查线程问题的基础。
public enum State {NEW, // 新建:线程已创建但未调用start()RUNNABLE, // 可运行:包含操作系统的运行中和就绪状态BLOCKED, // 阻塞:等待获取监视器锁WAITING, // 等待:无超时等待其他线程通知TIMED_WAITING,// 计时等待:有超时的等待TERMINATED // 终止:线程执行完毕
}
状态转换的关键节点:
NEW → RUNNABLE:调用start()
方法(注意不是run()
)
Thread t = new Thread();
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE(大概率)
RUNNABLE → BLOCKED:获取synchronized
锁失败时
进入 BLOCKED 状态,等待锁释放
RUNNABLE → WAITING:调用Object.wait()
、Thread.join()
等方法
必须等待其他线程调用notify()
/notifyAll()
才能唤醒
RUNNABLE → TIMED_WAITING:调用Thread.sleep(long)
、Object.wait(long)
等
无需其他线程通知,超时后自动唤醒
所有状态 → TERMINATED:run()
方法执行完毕或抛出未捕获异常
注意:Java 的
RUNNABLE
状态包含两种情况:
- 线程正在 CPU 上执行
- 线程处于就绪状态,等待 CPU 调度
这一点与操作系统的线程状态不同,需要特别注意。
五、实战:用工具观察线程状态
理论讲完了,我们用实际代码和工具来观察线程状态。
5.1 代码示例:创建不同状态的线程
public class ThreadStateDemo {public static void main(String[] args) throws InterruptedException {// 1. NEW状态Thread newThread = new Thread(() -> {});System.out.println("newThread状态: " + newThread.getState());// 2. RUNNABLE状态Thread runnableThread = new Thread(() -> {while (true) { // 无限循环,保持运行状态}});runnableThread.start();Thread.sleep(100); // 等待线程启动System.out.println("runnableThread状态: " + runnableThread.getState());// 3. BLOCKED状态Object lock = new Object();Thread blockedThread1 = new Thread(() -> {synchronized (lock) {try {Thread.sleep(10000); // 持有锁并休眠} catch (InterruptedException e) {e.printStackTrace();}}});blockedThread1.start();Thread.sleep(100); // 确保blockedThread1先获取锁Thread blockedThread2 = new Thread(() -> {synchronized (lock) { // 尝试获取已被持有的锁System.out.println("blockedThread2获取到锁");}});blockedThread2.start();Thread.sleep(100);System.out.println("blockedThread2状态: " + blockedThread2.getState());// 4. WAITING状态Thread waitingThread = new Thread(() -> {synchronized (lock) {try {lock.wait(); // 无超时等待} catch (InterruptedException e) {e.printStackTrace();}}});waitingThread.start();Thread.sleep(100);System.out.println("waitingThread状态: " + waitingThread.getState());// 5. TIMED_WAITING状态Thread timedWaitingThread = new Thread(() -> {try {Thread.sleep(10000); // 计时等待} catch (InterruptedException e) {e.printStackTrace();}});timedWaitingThread.start();Thread.sleep(100);System.out.println("timedWaitingThread状态: " + timedWaitingThread.getState());// 6. TERMINATED状态Thread terminatedThread = new Thread(() -> {});terminatedThread.start();Thread.sleep(100); // 等待线程执行完毕System.out.println("terminatedThread状态: " + terminatedThread.getState());// 销毁所有线程(示例用,实际开发不建议)runnableThread.interrupt();blockedThread1.interrupt();waitingThread.interrupt();timedWaitingThread.interrupt();}
}
运行结果(可能因环境略有差异):
newThread状态: NEW
runnableThread状态: RUNNABLE
blockedThread2状态: BLOCKED
waitingThread状态: WAITING
timedWaitingThread状态: TIMED_WAITING
terminatedThread状态: TERMINATED
5.2 用 jstack 查看线程状态
jstack 是 JDK 自带的工具,能打印 Java 进程的线程栈信息,是排查线程问题的利器。
1.先找到程序的进程 ID(PID):
jps -l
输出类似:
12345 ThreadStateDemo
2. 用 jstack 打印线程信息:
jstack 12345
3.在输出中找到我们创建的线程,例如 BLOCKED 状态的线程:
"Thread-2" #12 prio=5 os_prio=31 tid=0x00007f8a1a0a000 nid=0x5a03 waiting for monitor entry [0x000070000f9f3000]
java.lang.Thread.State: BLOCKED (on object monitor)
at ThreadStateDemo.lambda$main$2(ThreadStateDemo.java:35)
- waiting to lock <0x000000076ab36f80> (a java.lang.Object)
at ThreadStateDemo$$Lambda$3/0x0000000800060840.run(Unknown Source)
at java.lang.Thread.run(Thread.java:833)
通过 jstack,你可以清晰地看到线程的状态、等待的锁、调用栈等信息,这在排查死锁、线程阻塞等问题时非常有用。
六、新手常见误区与最佳实践
6.1 误区 1:线程越多,程序越快
最佳实践:根据任务类型设置合理的线程数:
CPU 密集型任务:线程数 ≈ CPU 核心数
IO 密集型任务:线程数 ≈ CPU 核心数 * 2(或根据 IO 等待时间调整)
6.2 误区 2:直接调用 run () 方法启动线程
很多新手会犯这样的错误:
Thread t = new Thread(() -> System.out.println("运行中"));
t.run(); // 错误:直接调用run()不会启动新线程
正确做法:调用start()
方法,它会通知 JVM 创建新线程并执行run()
:
t.start(); // 正确:启动新线程
6.3 误区 3:用 stop () 方法终止线程
Thread.stop()
已被废弃,因为它会强制终止线程,可能导致资源未释放、数据不一致等问题。
正确做法:用interrupt()
配合标志位终止线程:
Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {// 执行任务}System.out.println("线程安全终止");
});
t.start();// 终止线程
t.interrupt();
七、总结与下一篇预告
本文从操作系统底层到 Java 线程模型,讲解了线程的本质:
线程是 CPU 调度的基本单位,比进程更轻量
Java 采用 1:1 线程模型,线程状态与操作系统状态有映射关系
线程不是越多越好,上下文切换有性能代价
理解这些基础,是掌握多线程编程的前提。下一篇《多线程入门到精通系列: JDK源码理解3种方式与核心API》,我们将详细讲解 Java 创建线程的 3 种方式,以及start()
、join()
、sleep()
等核心方法的正确用法,敬请期待。