并发 vs 并行 vs 串行
搞清楚这三个概念,并发编程就懂了一半。
先看现象
你一定见过这种情况:
电脑同时开着浏览器、微信、IDE——三个程序同时运行。
但你的 CPU 可能只有 4 核。
4 核怎么同时跑 3 个程序?还是说其实不是"同时"?要回答这个问题,就得搞清楚并发、并行、串行这三个概念。
三种执行模式
串行:老老实实一个个来
一件事做完再做下一件,顺序执行,不可能有重叠。
java
// 串行执行
task1(); // 等 task1 完全结束
task2(); // 再开始 task2
task3(); // 最后 task3
// 总耗时 = task1 + task2 + task3特点:
- 简单,不会有混乱
- 效率低,CPU 经常闲着(等 I/O)
- 适合任务之间有依赖的场景
并发:交替执行,假装同时
单核 CPU 同一时刻只能执行一个任务,但 CPU 切换速度快到人类感知不到,看起来就像"同时"在运行。
时间 ────────────────────────────────────────────→
CPU: ████ task1 ████ task2 ████ task1 ████ task3 ████
↑ ↑ ↑
处理中 切换 切换
I/O: ░░░░ 等待中 ░░░░ ░░░░ 等待中 ░░░░
↑ ↑
阻塞 阻塞特点:
- 单核就能实现
- 充分利用 CPU(一个任务等 I/O,切换去执行另一个)
- 实质是"时分复用",快速切换
并行:真正的同时执行
多个任务在同一时刻同时执行,必须有多核 CPU 支持。
时间 ────────────────────────────────────────────→
CPU1: ████████████████████████████████ task1
CPU2: ████████████████████████████████ task2
CPU3: ████████████████████████████████ task3
同一时刻,三个任务真正同时运行特点:
- 需要多核 CPU
- 真正的同时执行,不切换
- 效率最高,但受硬件限制
一张图说清楚
┌─────────────────────────────────────┐
│ 任务执行方式 │
└─────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 串行 │ │ 并发 │ │ 并行 │
│ │ │ │ │ │
│ 一个个来 │ │ 交替执行 │ │ 同时执行 │
└──────────┘ └──────────┘ └──────────┘
│ │ │
▼ ▼ ▼
单线程顺序执行 单核快速切换 多核真正同时
场景: 场景: 场景:
- 简单任务 - I/O 密集型 - CPU 密集型
- 有依赖关系的任务 - 提高资源利用率 - 大数据处理实战区分
CPU 密集型 vs I/O 密集型
java
public class ConcurrencyVsParallel {
public static void main(String[] args) {
int cores = Runtime.getRuntime().availableProcessors();
System.out.println("你的 CPU 有 " + cores + " 个核心");
// CPU 密集型:计算为主,最好用并行
// 并行 = CPU 核心数 个线程
// I/O 密集型:等待为主,并发效果更好
// 因为等待时 CPU 闲着,可以切换去干别的
// 并发数 = CPU 核心数 * 2 或更多
}
}代码示例:并发下载文件
java
public class FileDownloader {
// 串行下载:3 个文件,总耗时 = 文件1 + 文件2 + 文件3
public void downloadSerial(List<String> urls) {
for (String url : urls) {
download(url); // 同步等待
}
}
// 并发下载:3 个文件同时下载,总耗时 ≈ max(文件1, 文件2, 文件3)
public void downloadConcurrent(List<String> urls) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (String url : urls) {
executor.submit(() -> download(url));
}
executor.shutdown();
}
private void download(String url) {
// 模拟下载耗时
System.out.println("开始下载: " + url);
try {
Thread.sleep(1000); // 假设 1 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("下载完成: " + url);
}
}输出对比(串行):
开始下载: 文件1
下载完成: 文件1
开始下载: 文件2
下载完成: 文件2
开始下载: 文件3
下载完成: 文件3
总耗时: 3 秒输出对比(并发):
开始下载: 文件1
开始下载: 文件2
开始下载: 文件3
下载完成: 文件1
下载完成: 文件2
下载完成: 文件3
总耗时: 1 秒常见误区
误区一:并发就是并行
很多人混用这两个词,但它们不是一回事:
| 并发 | 并行 | |
|---|---|---|
| 英文 | Concurrent | Parallel |
| 同一时刻 | 只有一个任务在执行 | 多个任务同时执行 |
| 硬件要求 | 单核即可 | 必须多核 |
| 比喻 | 一心多用(切换很快) | 多人同时工作 |
误区二:线程越多越好
线程数 = CPU 核心数 * 2?错!
I/O 密集型:可以多线程(等待时让 CPU 干别的)
CPU 密集型:线程数 ≈ CPU 核心数(再多没意义,CPU 忙不过来)
线程太多反而有害:
- 线程创建和切换有开销
- 内存占用增加
- 上下文切换反而降低效率误区三:并发一定能提速
java
// 场景:3 个任务,每个 100ms
// 串行:300ms
// 并发:100ms(理想情况)
// 但如果任务太短(比如只有 1ms),切换线程的开销可能比任务本身还大
// 这时候并发反而更慢
// 还有一种情况:任务之间有依赖,不能并行
task1();
task2(task1_result); // task2 依赖 task1 的结果
task3(task2_result); // task3 依赖 task2 的结果
// 只能串行总结
| 模式 | 同一时刻任务数 | 硬件要求 | 适用场景 |
|---|---|---|---|
| 串行 | 1 | 单核 | 简单任务、有依赖 |
| 并发 | 1(快速切换) | 单核 | I/O 密集、提高利用率 |
| 并行 | N | 多核 | CPU 密集、真正并行 |
记住:并发解决的是「抢着做」的问题,并行解决的是「一起做」的问题。
下一节我们看进程和线程的关系,以及 Java 中如何操作它们。
