为什么需要并发
你的 CPU 有 8 核,但你可能只用到了 1 核。
凌晨 2 点的服务器
你部署了一个 Java Web 服务,用户访问数据库时,SQL 执行需要 100ms。
不用并发时:
用户1: ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
↑ 请求处理 100ms ↑ 等数据库 100ms(CPU 空转)
用户2: ██████████████████████░░░░░░░
等待用户1完成 ↑ 又等 100ms
用户3: ██████████████████████░░░
又等了 200msCPU 利用率:50%(处理请求时忙,等数据库时闲着)
用并发时:
用户1: ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
↑ 处理 10ms ↑ CPU 切去处理用户2
用户2: ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
↑ 处理 10ms ↑ CPU 切去处理用户3
用户3: ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░CPU 利用率:提升到 80%+
多核时代:硬件变了
┌─────────────────────────────────────────────────────────┐
│ CPU 发展历史 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1970s-2000s: 单核时代 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 频率: 1MHz ──────→ 3GHz │ │
│ │ 编译器优化 ──────→ 指令并行 │ │
│ └─────────────────────────────────────────────────┘ │
│ ↓ │
│ 2000s-至今: 多核时代 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 核心数: 1 ──→ 2 ──→ 4 ──→ 8 ──→ 16 ──→ 64... │ │
│ │ 单核性能提升变缓,靠增加核心数提升性能 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘你的手机可能是 8 核,你的电脑可能是 16 核。
但如果你写的是单线程程序,你只用了其中 1 核。
并发解决的核心问题
问题一:I/O 阻塞
CPU 计算速度是纳秒级,I/O 等待是毫秒级。差距 100 万倍。
java
public class IOBlockingDemo {
public static void main(String[] args) {
// 模拟:读取 3 个文件
// 串行:3 秒(每个 1 秒)
// 并发:1 秒(同时读取)
long start = System.nanoTime();
readFileSequential("file1");
readFileSequential("file2");
readFileSequential("file3");
System.out.println("串行耗时: " + (System.nanoTime() - start) / 1_000_000 + "ms");
// 并发读取
start = System.nanoTime();
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> readFileConcurrent("file1"));
executor.submit(() -> readFileConcurrent("file2"));
executor.submit(() -> readFileConcurrent("file3"));
executor.shutdown();
try { executor.awaitTermination(10, TimeUnit.SECONDS); } catch (Exception e) {}
System.out.println("并发耗时: " + (System.nanoTime() - start) / 1_000_000 + "ms");
}
private static void readFileSequential(String file) {
try {
System.out.println("读取: " + file);
Thread.sleep(1000); // 模拟 I/O 耗时
} catch (InterruptedException e) {}
}
private static void readFileConcurrent(String file) {
try {
System.out.println("并发读取: " + file + " by " + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {}
}
}问题二:响应性
用户点击按钮后,如果处理在主线程,你的界面就会卡死。
java
public class ResponsivenessDemo {
public static void main(String[] args) {
System.out.println("用户点击按钮");
// ❌ 错误:在主线程执行耗时操作,界面卡死
// doLongRunningTask();
// ✅ 正确:后台线程执行,主线程立即返回
CompletableFuture.runAsync(() -> doLongRunningTask());
System.out.println("按钮释放,用户可以继续操作");
}
private static void doLongRunningTask() {
try {
Thread.sleep(5000);
System.out.println("后台任务完成");
} catch (InterruptedException e) {}
}
}问题三:并行计算
充分利用多核 CPU。
java
public class ParallelComputing {
public static void main(String[] args) throws InterruptedException {
int cores = Runtime.getRuntime().availableProcessors();
System.out.println("CPU 核心数: " + cores);
// 大数据处理:计算 1 亿个数字的和
// 单线程:约 1000ms
// 多线程:约 1000ms / cores
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100_000_000; i++) {
sum += i;
}
long singleThreadTime = (System.nanoTime() - start) / 1_000_000;
System.out.println("单线程耗时: " + singleThreadTime + "ms, 结果: " + sum);
// 多线程
start = System.nanoTime();
ExecutorService executor = Executors.newFixedThreadPool(cores);
long chunkSize = 100_000_000 / cores;
long[] results = new long[cores];
for (int i = 0; i < cores; i++) {
final int index = i;
final long startNum = index * chunkSize;
final long endNum = (i == cores - 1) ? 100_000_000 : startNum + chunkSize;
executor.submit(() -> {
long localSum = 0;
for (long j = startNum; j < endNum; j++) {
localSum += j;
}
results[index] = localSum;
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
long total = 0;
for (long r : results) total += r;
long multiThreadTime = (System.nanoTime() - start) / 1_000_000;
System.out.println("多线程耗时: " + multiThreadTime + "ms, 结果: " + total);
}
}实际应用场景
| 场景 | 说明 | 并发作用 |
|---|---|---|
| Web 服务器 | 每个请求一个线程 | Tomcat 默认 200 线程并发处理 |
| 数据库连接池 | 复用连接 | HikariCP 数十个连接服务上千请求 |
| 消息队列 | 异步处理 | Kafka 多消费者并行消费 |
| 大数据处理 | 分片并行 | Spark 分布式并行计算 |
| 爬虫 | 并发请求 | 多线程同时抓取多个页面 |
| GUI 应用 | 后台线程 | 不阻塞用户界面 |
并发 vs 单线程:性能对比
java
public class PerformanceComparison {
public static void main(String[] args) throws InterruptedException {
int cores = Runtime.getRuntime().availableProcessors();
System.out.println("CPU 核心数: " + cores);
// 场景:处理 1000 个 I/O 任务,每个 10ms
// 单线程:1000 * 10ms = 10 秒
// 10 线程并发:1000 / 10 * 10ms = 1 秒
int taskCount = 1000;
int concurrency = Math.min(taskCount, cores * 10); // I/O 密集型可以更多线程
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
long start = System.nanoTime();
CountDownLatch latch = new CountDownLatch(taskCount);
for (int i = 0; i < taskCount; i++) {
executor.submit(() -> {
try {
Thread.sleep(10); // 模拟 I/O
} catch (InterruptedException e) {}
latch.countDown();
});
}
latch.await();
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("1000 个 I/O 任务,并发度 " + concurrency);
System.out.println("总耗时: " + duration + "ms");
System.out.println("吞吐量: " + (taskCount * 1000.0 / duration) + " 任务/秒");
executor.shutdown();
}
}何时不用并发?
并发不是万能的,有些场景反而应该用单线程:
| 场景 | 原因 | 例子 |
|---|---|---|
| 任务太短 | 创建线程开销大于任务本身 | 计算 1+1 |
| 有强依赖 | 后一个依赖前一个的结果 | A 的结果是 B 的输入 |
| 单核且无 I/O | 并发反而增加切换开销 | CPU 计算任务在单核上 |
| 调试困难 | 并发问题难复现 | 简单脚本 |
总结
为什么需要并发?
- 多核 CPU 时代:硬件变了,一核打天下的时代过去了
- I/O 阻塞:CPU 和 I/O 速度差距 100 万倍,等着不如干别的
- 响应性:用户等不及,后台任务异步处理
- 吞吐量:并发才能压榨服务器性能
记住:
- 并发 ≠ 线程越多越好
- I/O 密集型可以用更多线程(因为大部分时间在等)
- CPU 密集型线程数 ≈ CPU 核心数
- 优先用线程池,避免手动创建线程
下一节我们深入讨论线程安全问题。
