Skip to content

为什么需要并发

你的 CPU 有 8 核,但你可能只用到了 1 核。

凌晨 2 点的服务器

你部署了一个 Java Web 服务,用户访问数据库时,SQL 执行需要 100ms。

不用并发时

用户1: ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
       ↑ 请求处理 100ms   ↑ 等数据库 100ms(CPU 空转)

用户2:                                    ██████████████████████░░░░░░░
                                      等待用户1完成          ↑ 又等 100ms

用户3:                                                  ██████████████████████░░░
                                                    又等了 200ms

CPU 利用率: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 计算任务在单核上
调试困难并发问题难复现简单脚本

总结

为什么需要并发?

  1. 多核 CPU 时代:硬件变了,一核打天下的时代过去了
  2. I/O 阻塞:CPU 和 I/O 速度差距 100 万倍,等着不如干别的
  3. 响应性:用户等不及,后台任务异步处理
  4. 吞吐量:并发才能压榨服务器性能

记住

  • 并发 ≠ 线程越多越好
  • I/O 密集型可以用更多线程(因为大部分时间在等)
  • CPU 密集型线程数 ≈ CPU 核心数
  • 优先用线程池,避免手动创建线程

下一节我们深入讨论线程安全问题。

基于 VitePress 构建