进程 vs 线程 vs 协程
先搞懂这三个「执行单位」,后面的内容才不迷糊。
一个故事
想象你在运营一家餐厅:
进程 = 餐厅本身(独立的资源:厨房、餐桌、人员)
线程 = 餐厅里的员工(共享厨房和食材,但各忙各的)
协程 = 员工里的「高效多面手」(不需要经理协调,自己切换任务)进程是资源分配的最小单位,线程是 CPU 调度的最小单位,协程是更轻量的协作式任务单元。
进程(Process)
进程是程序的一次执行,是操作系统分配资源的基本单位。
当你双击打开一个 Java 程序,操作系统就会创建一个进程。这个进程有自己独立的:
- 内存空间(代码段、数据段、堆、栈)
- 文件句柄(打开的文件、网络连接)
- 进程 ID(PID)
┌─────────────────────────────────────────────────────────┐
│ 进程 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 进程控制块 (PCB) │ │
│ │ PID │ 状态 │ 程序计数器 │ 寄存器 │ 调度信息 │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 进程内存空间 │ │
│ │ 代码段 │ 数据段 │ BSS段 │ 堆 │ 栈 │ 环境变量 │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 文件句柄 / 网络连接 / 其他系统资源 │
│ │
└─────────────────────────────────────────────────────────┘Java 中获取进程信息
java
public class ProcessDemo {
public static void main(String[] args) {
// 获取当前进程
long pid = ProcessHandle.current().pid();
System.out.println("当前进程 ID: " + pid);
// 获取当前进程信息
ProcessHandle.current().info().forEach(info ->
System.out.println(info.toString())
);
// Java 19+ 可以获取子进程
// ProcessHandle.current().children().forEach(...);
}
}进程的特点:
- 独立运行,一个进程崩溃不会影响其他进程
- 创建和销毁开销大(需要分配独立内存空间)
- 进程间通信(IPC)需要额外机制:管道、消息队列、Socket
线程(Thread)
线程是进程内的执行单元,是 CPU 调度的最小单位。
一个进程可以包含多个线程,所有线程共享进程的内存空间(堆、方法区),但每个线程有自己独立的栈。
┌─────────────────────────────────────────────────────────┐
│ 进程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 线程1 线程2 线程3 线程4 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ PC, SP │ │ PC, SP │ │ PC, SP │ │ PC, SP │ │
│ │ 寄存器 │ │ 寄存器 │ │ 寄存器 │ │ 寄存器 │ │
│ │ 栈 │ │ 栈 │ │ 栈 │ │ 栈 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 共享区域 │ │
│ │ 代码段 │ 数据段 │ 堆 │ 文件句柄 │ 网络连接 │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘PC = 程序计数器(下一条指令),SP = 栈指针(当前栈帧)
Java 中操作线程
java
public class ThreadDemo {
public static void main(String[] args) {
// 获取主线程
Thread mainThread = Thread.currentThread();
System.out.println("主线程: " + mainThread.getName());
System.out.println("主线程 ID: " + mainThread.getId());
// 创建新线程
Thread worker = new Thread(() -> {
System.out.println("工作线程: " + Thread.currentThread().getName());
System.out.println("工作线程 ID: " + Thread.currentThread().getId());
}, "Worker");
worker.start();
// 线程状态
System.out.println("启动前状态: " + worker.getState());
System.out.println("是否存活: " + worker.isAlive());
}
}线程的特点:
- 共享进程资源(堆内存、文件句柄),通信方便
- 创建和销毁开销小
- 一个线程崩溃,可能导致整个进程崩溃
- 需要同步机制(因为共享数据)
协程(Coroutine)
协程是比线程更轻量的执行单元,是一种协作式的任务调度方式。
为什么需要协程?
线程虽小,但也有开销:
- 创建和切换需要内核态操作
- 每个线程默认占用 1MB 栈空间
- 线程太多,上下文切换成本高
协程的特点:
- 用户态切换,不需要内核操作
- 栈空间可以很小(几 KB)
- 协作式调度:主动让出 CPU,不抢占
线程:抢占式调度(操作系统决定谁运行)
协程:协作式调度(自己决定何时让出)Java 中的协程
Java 21 引入了虚拟线程(Virtual Threads),这是 JDK 层面的协程实现:
java
// 传统线程
Thread thread = Thread.ofPlatform().name("platform").start(() -> {
// ...
});
// 虚拟线程(协程)
Thread virtualThread = Thread.ofVirtual().name("virtual").start(() -> {
// ...
});
// 使用 Thread.startVirtualThread() 更简洁
Thread.startVirtualThread(() -> {
System.out.println("虚拟线程运行中");
});虚拟线程 vs 传统线程:
| 对比项 | 传统线程 | 虚拟线程 |
|---|---|---|
| 创建开销 | 大(1MB 栈空间) | 小(几百字节) |
| 切换成本 | 内核态,高 | 用户态,低 |
| 数量 | 数千个就很多了 | 可以轻松创建数百万个 |
| 调度 | 操作系统抢占式 | JDK 协作式 |
协程适用场景
java
public class CoroutineDemo {
public static void main(String[] args) throws InterruptedException {
// 场景:处理大量 I/O 任务
// 传统方式需要创建很多线程,开销大
// 虚拟线程可以轻松创建数百万个
long start = System.nanoTime();
// 创建 10 万个虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100_000).forEach(i ->
executor.submit(() -> {
// 模拟 I/O 操作
try {
Thread.sleep(Duration.ofSeconds(1));
} catch (InterruptedException e) {
e.printStackTrace();
}
return i;
})
);
}
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("10 万个任务耗时: " + duration + "ms");
}
}三者对比
| 对比项 | 进程 | 线程 | 协程(虚拟线程) |
|---|---|---|---|
| 定义 | 程序执行实例 | 进程内的执行单元 | 更轻量的执行单元 |
| 资源分配 | 操作系统分配 | 共享进程资源 | 共享线程资源 |
| 创建开销 | 大 | 中 | 极小 |
| 切换开销 | 大 | 中 | 极小 |
| 通信 | IPC(管道/消息队列) | 直接共享内存 | 直接共享内存 |
| 隔离性 | 高(独立地址空间) | 低(共享地址空间) | 低(共享地址空间) |
| 调度方式 | 操作系统抢占式 | 操作系统抢占式 | 协作式 |
实战:理解 Java 程序
java
public class JavaProcessDemo {
public static void main(String[] args) {
// 当你运行这个程序:
// 1. 操作系统创建一个进程
// 2. JVM 启动一个主线程
// 3. main() 在主线程中运行
System.out.println("进程 ID: " + ProcessHandle.current().pid());
System.out.println("主线程: " + Thread.currentThread().getName());
System.out.println("主线程 ID: " + Thread.currentThread().getId());
// 创建新线程
new Thread(() -> {
System.out.println("新线程: " + Thread.currentThread().getName());
System.out.println("新线程 ID: " + Thread.currentThread().getId());
}, "MyThread").start();
// 虚拟线程(Java 21+)
Thread.startVirtualThread(() ->
System.out.println("虚拟线程: " + Thread.currentThread().getName())
);
}
}运行结果示例:
进程 ID: 12345
主线程: main
主线程 ID: 1
新线程: MyThread
新线程 ID: 20
虚拟线程: VirtualThread-1总结
- 进程是资源分配的单位,独立运行,一个崩溃不影响另一个
- 线程是 CPU 调度的单位,共享进程资源,切换开销小
- 协程是比线程更轻量的执行单元,用户态切换,适合大量 I/O 任务
- Java 21+ 的虚拟线程让协程编程变得简单
记住:进程是「房子」,线程是「住在房子里的人」,协程是「人的超能力——不需要经理分配,自己决定何时切换任务」。
下一节我们看线程的创建方式,以及哪种方式更好。
