引言:Chrome的选择
打开Chrome浏览器的任务管理器(Shift + Esc),你会看到:
任务管理器
┌──────────────────────┬────────┬────────┐
│ 任务 │ 内存 │ CPU │
├──────────────────────┼────────┼────────┤
│ 浏览器进程 │ 80 MB │ 1% │
│ GPU进程 │ 120 MB │ 2% │
│ 标签页: 百度 │ 65 MB │ 0% │
│ 标签页: GitHub │ 95 MB │ 3% │
│ 标签页: YouTube │ 150 MB │ 15% │
│ 扩展程序: AdBlock │ 25 MB │ 0% │
└──────────────────────┴────────┴────────┘
为什么Chrome选择多进程架构,而不是多线程?
- 如果用多线程,YouTube标签页崩溃 → 整个浏览器崩溃
- 如果用多进程,YouTube标签页崩溃 → 只影响一个标签页
这背后涉及进程与线程的本质区别。
一、进程:资源分配的基本单位
1.1 什么是进程?
定义:进程(Process)是操作系统进行资源分配和调度的基本单位。
更直白的解释:进程是一个正在运行的程序实例。
// 在Java中启动一个新进程
public class ProcessExample {
public static void main(String[] args) throws IOException {
// 启动记事本程序(新进程)
ProcessBuilder pb = new ProcessBuilder("notepad.exe");
Process process = pb.start();
System.out.println("新进程的PID: " + process.pid());
System.out.println("新进程是否存活: " + process.isAlive());
}
}
1.2 进程的组成
一个进程包含以下资源:
进程的内存布局(以32位系统为例)
┌─────────────────────┐ ← 0xFFFFFFFF (4GB)
│ 内核空间 │ (操作系统使用,进程不可访问)
│ (1GB) │
├─────────────────────┤ ← 0xC0000000 (3GB)
│ │
│ 栈 (Stack) │ ← 向下增长
│ - 局部变量 │
│ - 函数调用栈 │
│ │
│ ↓ │
│ │
│ (未使用空间) │
│ │
│ ↑ │
│ │
│ 堆 (Heap) │ ← 向上增长
│ - 动态分配内存 │
│ - new/malloc │
│ │
├─────────────────────┤
│ BSS段 │ (未初始化的全局变量)
├─────────────────────┤
│ Data段 │ (已初始化的全局变量)
├─────────────────────┤
│ Text段 (代码段) │ (程序代码,只读)
└─────────────────────┘ ← 0x00000000
进程独有的资源:
内存空间:
- 代码段(Text):程序代码
- 数据段(Data/BSS):全局变量
- 堆(Heap):动态分配的内存
- 栈(Stack):函数调用和局部变量
文件描述符:
- 打开的文件
- Socket连接
- 管道等
进程ID(PID):
- 系统唯一标识
环境变量:
- PATH、HOME等
1.3 进程的隔离性
关键特性:不同进程之间是相互隔离的。
// 进程A
public class ProcessA {
private static int count = 0;
public static void main(String[] args) {
count = 100;
System.out.println("进程A的count: " + count);
}
}
// 进程B(同时运行)
public class ProcessB {
private static int count = 0;
public static void main(String[] args) {
count = 200;
System.out.println("进程B的count: " + count);
// 无法访问进程A的count变量
}
}
输出:
进程A的count: 100
进程B的count: 200
进程A和进程B的count变量是完全独立的,互不影响。
二、线程:CPU调度的基本单位
2.1 什么是线程?
定义:线程(Thread)是操作系统进行CPU调度的基本单位。
形象比喻:
- 进程 = 一个公司
- 线程 = 公司里的员工
关键点:同一个进程内的线程共享进程的资源。
public class ThreadExample {
private static int sharedCount = 0; // 共享变量
public static void main(String[] args) {
// 线程1
Thread t1 = new Thread(() -> {
sharedCount = 100;
System.out.println("线程1修改后: " + sharedCount);
});
// 线程2
Thread t2 = new Thread(() -> {
System.out.println("线程2读取到: " + sharedCount);
});
t1.start();
try { Thread.sleep(100); } catch (InterruptedException e) {}
t2.start();
}
}
输出:
线程1修改后: 100
线程2读取到: 100 ← 线程2能看到线程1的修改
2.2 线程的组成
一个线程包含以下私有资源:
线程的私有资源
┌─────────────────────┐
│ 程序计数器 (PC) │ ← 指向下一条要执行的指令
├─────────────────────┤
│ 虚拟机栈 │ ← 存储局部变量和方法调用
│ ┌────────────┐ │
│ │ 栈帧 3 │ │
│ ├────────────┤ │
│ │ 栈帧 2 │ │
│ ├────────────┤ │
│ │ 栈帧 1 │ │
│ └────────────┘ │
├─────────────────────┤
│ 本地方法栈 │ ← native方法调用
├─────────────────────┤
│ 线程ID (TID) │ ← 线程的唯一标识
└─────────────────────┘
线程私有的资源(不共享):
程序计数器(PC):
- 记录当前线程执行到哪一行代码
- 每个线程独立
虚拟机栈:
- 局部变量
- 方法参数
- 方法返回地址
线程ID:
- 线程的唯一标识
线程共享的资源(来自进程):
堆内存:
new出来的对象- 静态变量
方法区:
- 类信息
- 常量池
文件描述符:
- 打开的文件
- Socket连接
2.3 一个进程内的多个线程
进程的内存空间
┌──────────────────────────────────┐
│ 进程 A │
│ ┌────────────────────────────┐ │
│ │ 堆 (Heap) │ │ ← 所有线程共享
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │Object│ │Object│ │ │
│ │ └──────┘ └──────┘ │ │
│ └────────────────────────────┘ │
│ │
│ ┌────────────────────────────┐ │
│ │ 方法区 (Method Area) │ │ ← 所有线程共享
│ │ - 类信息 │ │
│ │ - 静态变量 │ │
│ └────────────────────────────┘ │
│ │
│ 线程1 线程2 线程3 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ PC │ │ PC │ │ PC │ │ ← 每个线程私有
│ ├─────┤ ├─────┤ ├─────┤ │
│ │Stack│ │Stack│ │Stack│ │ ← 每个线程私有
│ └─────┘ └─────┘ └─────┘ │
└──────────────────────────────────┘
三、进程 vs 线程:核心对比
| 对比维度 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU调度的基本单位 |
| 资源占用 | 重量级(MB级) | 轻量级(KB级) |
| 内存空间 | 独立的地址空间 | 共享进程的地址空间 |
| 通信方式 | IPC(管道、消息队列、共享内存) | 直接读写共享变量 |
| 通信复杂度 | 复杂,需要系统调用 | 简单,但需要同步控制 |
| 创建开销 | 大(需要分配内存、复制资源) | 小(只需创建栈和PC) |
| 创建速度 | 慢(毫秒级) | 快(微秒级) |
| 切换开销 | 大(需要切换地址空间、刷新TLB) | 小(只需切换栈和PC) |
| 隔离性 | 强(一个进程崩溃不影响其他进程) | 弱(一个线程崩溃导致整个进程崩溃) |
| 数据安全性 | 高(进程间相互隔离) | 低(需要加锁保护共享数据) |
| 典型应用 | 浏览器的多标签页、微服务架构 | Web服务器处理并发请求 |
3.1 资源占用对比实验
public class ProcessVsThreadTest {
public static void main(String[] args) throws Exception {
// 测试1:创建进程的开销
long start1 = System.nanoTime();
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
process.waitFor();
long time1 = System.nanoTime() - start1;
// 测试2:创建线程的开销
long start2 = System.nanoTime();
Thread thread = new Thread(() -> {
// 空任务
});
thread.start();
thread.join();
long time2 = System.nanoTime() - start2;
System.out.println("创建进程耗时: " + time1 / 1_000_000.0 + "ms");
System.out.println("创建线程耗时: " + time2 / 1_000_000.0 + "ms");
System.out.println("进程/线程耗时比: " + (double)time1 / time2);
}
}
典型输出:
创建进程耗时: 45.2ms
创建线程耗时: 0.8ms
进程/线程耗时比: 56.5x
结论:创建进程的开销是创建线程的50-100倍。
3.2 通信方式对比
进程间通信(IPC)- 复杂
// 使用管道进行进程间通信
public class ProcessCommunication {
public static void main(String[] args) throws Exception {
// 父进程创建管道
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out);
// 启动子进程(实际中需要通过ProcessBuilder)
// 这里简化演示原理
ProcessBuilder pb = new ProcessBuilder("子进程命令");
// ... 复杂的IPC配置 ...
}
}
线程间通信 - 简单
// 线程间共享变量
public class ThreadCommunication {
private static String message = null; // 共享变量
public static void main(String[] args) throws Exception {
// 线程1:写入
Thread writer = new Thread(() -> {
message = "Hello from Thread 1";
});
// 线程2:读取
Thread reader = new Thread(() -> {
while (message == null) {
// 等待消息
}
System.out.println("收到消息: " + message);
});
writer.start();
reader.start();
writer.join();
reader.join();
}
}
四、上下文切换:性能的隐形杀手
4.1 什么是上下文切换?
定义:CPU从一个线程切换到另一个线程时,需要保存当前线程的状态,恢复新线程的状态,这个过程叫上下文切换。
上下文切换过程
时刻 T1:线程A正在执行
┌─────────────┐
│ 线程A │
│ ┌───────┐ │
│ │ PC=100│ │ ← 程序计数器指向第100行
│ │ 寄存器 │ │ ← 各种寄存器的值
│ │ Stack │ │ ← 栈指针
│ └───────┘ │
└─────────────┘
发生中断(时间片到期/等待IO)
时刻 T2:保存线程A的状态
┌─────────────┐
│ 保存到内存 │
│ PC=100 │
│ 寄存器值 │
│ 栈指针 │
└─────────────┘
时刻 T3:恢复线程B的状态
┌─────────────┐
│ 从内存加载 │
│ PC=250 │
│ 寄存器值 │
│ 栈指针 │
└─────────────┘
时刻 T4:线程B开始执行
┌─────────────┐
│ 线程B │
│ ┌───────┐ │
│ │ PC=250│ │ ← 从第250行继续执行
│ │ 寄存器 │ │
│ │ Stack │ │
│ └───────┘ │
└─────────────┘
4.2 上下文切换的成本
进程切换的成本(高)
进程切换需要:
1. 保存进程A的状态
- 所有寄存器
- 程序计数器
- 栈指针
- ...
2. 切换地址空间
- 更新页表
- 刷新TLB(Translation Lookaside Buffer)
- 刷新CPU缓存(L1/L2/L3)← 最大的开销!
3. 恢复进程B的状态
- 加载所有寄存器
- 加载程序计数器
- 加载栈指针
- ...
总耗时:5-10微秒
线程切换的成本(低)
线程切换需要:
1. 保存线程A的状态
- 所有寄存器
- 程序计数器
- 栈指针
2. 不需要切换地址空间
- 页表不变
- TLB不需要刷新
- CPU缓存大部分可以保留 ← 省了最大的开销!
3. 恢复线程B的状态
- 加载所有寄存器
- 加载程序计数器
- 加载栈指针
总耗时:1-2微秒
结论:进程切换的成本是线程切换的3-5倍。
4.3 上下文切换的性能影响
public class ContextSwitchCost {
private static final int COUNT = 10_000_000;
public static void main(String[] args) throws Exception {
// 测试1:单线程(无上下文切换)
long start1 = System.nanoTime();
int sum1 = 0;
for (int i = 0; i < COUNT; i++) {
sum1 += i;
}
long time1 = System.nanoTime() - start1;
// 测试2:两个线程互相切换(大量上下文切换)
Object lock = new Object();
int[] sum2 = {0};
CountDownLatch latch = new CountDownLatch(2);
long start2 = System.nanoTime();
Thread t1 = new Thread(() -> {
for (int i = 0; i < COUNT / 2; i++) {
synchronized (lock) {
sum2[0] += i;
lock.notify();
try { lock.wait(); } catch (InterruptedException e) {}
}
}
latch.countDown();
});
Thread t2 = new Thread(() -> {
for (int i = COUNT / 2; i < COUNT; i++) {
synchronized (lock) {
sum2[0] += i;
lock.notify();
try { lock.wait(); } catch (InterruptedException e) {}
}
}
latch.countDown();
});
t1.start();
t2.start();
latch.await();
long time2 = System.nanoTime() - start1;
System.out.println("单线程耗时: " + time1 / 1_000_000.0 + "ms");
System.out.println("多线程(频繁切换)耗时: " + time2 / 1_000_000.0 + "ms");
System.out.println("性能下降: " + (double)time2 / time1 + "x");
}
}
典型输出:
单线程耗时: 25ms
多线程(频繁切换)耗时: 8500ms
性能下降: 340x
关键结论:过多的上下文切换会严重降低性能。
五、Java中的线程
5.1 Java线程模型
重要事实:Java线程是对操作系统线程的封装。
Java线程与操作系统线程的映射关系
Java应用层
┌─────────────────────────────────┐
│ Thread t1 = new Thread(...); │
│ t1.start(); │
└────────────┬────────────────────┘
│
│ JNI调用
↓
JVM层
┌─────────────────────────────────┐
│ HotSpot VM │
│ - 线程管理 │
│ - 线程调度 │
└────────────┬────────────────────┘
│
│ 系统调用
↓
操作系统层
┌─────────────────────────────────┐
│ Linux: pthread_create() │
│ Windows: CreateThread() │
│ macOS: pthread_create() │
└─────────────────────────────────┘
│
↓
内核线程 (Kernel Thread)
5.2 1:1 线程模型
现代JVM采用1:1线程模型:一个Java线程对应一个操作系统内核线程。
public class JavaThreadMapping {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Java线程ID: " + Thread.currentThread().getId());
System.out.println("Java线程名称: " + Thread.currentThread().getName());
// 获取操作系统线程ID(Java 9+)
long nativeThreadId = Thread.currentThread().threadId();
System.out.println("操作系统线程ID: " + nativeThreadId);
}, "MyThread");
thread.start();
}
}
输出:
Java线程ID: 12
Java线程名称: MyThread
操作系统线程ID: 23456 ← 操作系统分配的真实线程ID
5.3 Java线程的生命周期管理
public class ThreadLifecycle {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
System.out.println("线程开始执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("线程被中断");
}
System.out.println("线程执行结束");
});
// 1. NEW状态:线程对象创建,但未启动
System.out.println("状态1: " + thread.getState()); // NEW
// 2. 调用start()启动线程
thread.start();
Thread.sleep(100);
System.out.println("状态2: " + thread.getState()); // RUNNABLE
// 3. 线程sleep时
Thread.sleep(500);
System.out.println("状态3: " + thread.getState()); // TIMED_WAITING
// 4. 等待线程结束
thread.join();
System.out.println("状态4: " + thread.getState()); // TERMINATED
}
}
六、协程:用户态线程
6.1 什么是协程?
协程(Coroutine):也叫用户态线程,由程序自己调度,不需要操作系统参与。
线程 vs 协程
操作系统线程(内核态)
┌─────────────────┐
│ 操作系统负责调度 │ ← 需要系统调用,开销大
│ 线程1 │
│ 线程2 │
│ 线程3 │
└─────────────────┘
协程(用户态)
┌─────────────────┐
│ 线程1 │
│ ├─ 协程1.1 │ ← 程序自己调度,开销小
│ ├─ 协程1.2 │
│ └─ 协程1.3 │
│ │
│ 线程2 │
│ ├─ 协程2.1 │
│ └─ 协程2.2 │
└─────────────────┘
6.2 协程的优势
创建开销极小:
- 线程:几KB栈空间
- 协程:几百字节
切换开销极小:
- 线程切换:需要系统调用,1-2微秒
- 协程切换:用户态切换,几十纳秒
可以创建海量协程:
- 线程:受限于系统资源,通常几千个
- 协程:可以创建上百万个
6.3 Java中的虚拟线程(Project Loom)
Java 21+ 引入了虚拟线程(Virtual Threads),本质上就是协程。
// Java 21+ 的虚拟线程
public class VirtualThreadExample {
public static void main(String[] args) throws Exception {
// 传统平台线程
Thread platformThread = Thread.ofPlatform()
.name("platform-thread")
.start(() -> {
System.out.println("平台线程: " + Thread.currentThread());
});
// 虚拟线程(协程)
Thread virtualThread = Thread.ofVirtual()
.name("virtual-thread")
.start(() -> {
System.out.println("虚拟线程: " + Thread.currentThread());
});
platformThread.join();
virtualThread.join();
// 创建100万个虚拟线程(秒级完成)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
});
}
}
System.out.println("100万个虚拟线程创建完成");
}
}
对比:
| 特性 | 平台线程(OS Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
| 创建开销 | 几KB | 几百字节 |
| 切换开销 | 1-2微秒 | 几十纳秒 |
| 最大数量 | 几千个 | 百万级 |
| 阻塞成本 | 高(占用OS线程) | 低(不占用OS线程) |
| 适用场景 | CPU密集型 | IO密集型 |
七、实战:验证线程与进程的特性
7.1 验证进程隔离性
// ProcessA.java
public class ProcessA {
private static int value = 100;
public static void main(String[] args) throws Exception {
System.out.println("进程A的PID: " + ProcessHandle.current().pid());
System.out.println("进程A的value: " + value);
// 修改value
value = 200;
System.out.println("进程A修改后的value: " + value);
Thread.sleep(10000); // 保持进程运行
}
}
// ProcessB.java
public class ProcessB {
private static int value = 100;
public static void main(String[] args) {
System.out.println("进程B的PID: " + ProcessHandle.current().pid());
System.out.println("进程B的value: " + value); // 仍然是100,不受进程A影响
}
}
7.2 验证线程共享内存
public class ThreadSharing {
private static int sharedValue = 0;
public static void main(String[] args) throws Exception {
// 线程1:写入
Thread writer = new Thread(() -> {
sharedValue = 100;
System.out.println("线程1写入: " + sharedValue);
}, "Writer");
// 线程2:读取
Thread reader = new Thread(() -> {
try {
Thread.sleep(100); // 等待线程1写入
System.out.println("线程2读取: " + sharedValue); // 能读到100
} catch (InterruptedException e) {}
}, "Reader");
writer.start();
reader.start();
writer.join();
reader.join();
}
}
八、总结
8.1 核心要点
进程是资源分配的基本单位
- 拥有独立的地址空间
- 进程间相互隔离
- 创建和切换开销大
线程是CPU调度的基本单位
- 共享进程的地址空间
- 线程间可以直接通信
- 创建和切换开销小
进程 vs 线程选择
- 需要隔离性 → 选择进程(如Chrome浏览器)
- 需要高性能通信 → 选择线程(如Web服务器)
上下文切换是性能杀手
- 过多的上下文切换会严重降低性能
- 线程切换比进程切换开销小
- 协程切换比线程切换开销更小
Java线程模型
- 1:1映射到操作系统线程
- Java 21+ 引入虚拟线程(协程)
8.2 类比总结
| 概念 | 现实世界类比 |
|---|---|
| 进程 | 一个公司 |
| 线程 | 公司里的员工 |
| 进程隔离 | 不同公司的员工互不认识 |
| 线程共享 | 同一公司的员工共享办公室 |
| 上下文切换 | 员工换岗(需要交接工作) |
| 协程 | 一个员工同时处理多个任务 |
8.3 思考题
- 为什么浏览器用多进程而不是多线程?
- 什么场景下应该使用多进程而不是多线程?
- 上下文切换的成本主要来自哪里?
- 虚拟线程(协程)相比平台线程有什么优势?
8.4 下一篇预告
《Java线程的生命周期:6种状态详解》
- 线程的6种状态是什么?
- 状态之间如何转换?
- 如何使用jstack分析线程状态?
- BLOCKED和WAITING的区别?
扩展阅读
- Java Concurrency in Practice - Chapter 2: Thread Safety
- Linux线程实现
- Project Loom: Fibers and Continuations
- 协程的原理与实现