为什么需要并发编程

📋 概述

一句话:并发编程是为了解决"单线程处理不过来"的问题——让程序能同时做多件事,提升性能和响应速度。

从一个真实场景说起

想象一下:你开了一家网店,平时一天卖 100 单,用一个客服处理订单,绰绰有余。

突然双十一来了,同时涌入1000 个订单。如果你还坚持"一个客服处理完一个再处理下一个",会发生什么?

  • 第一个用户等了 10 分钟才拿到确认
  • 第十个用户等了 100 分钟,直接退款走人
  • 第一百个用户等到页面超时,再也不来了

问题的本质:单线程(单客服)的处理能力是有限的,当请求量超过处理速度时,系统就会崩溃。

解决方案:多雇几个客服(多线程),同时处理多个订单。这就是并发编程的动机

💡 生活类比:厨房做饭

单线程:一个人做饭

想象你一个人在厨房做饭,流程是这样的:

总耗时:5 + 3 + 5 + 1 =14 分钟。在这 14 分钟里,你一直在忙,但效率很低——洗菜时灶台是空的,切菜时水龙头是空的。

多线程:厨房多人协作

现在雇了三个人:你负责切菜,小王负责洗菜,小李负责炒菜。三个人分工合作:

总耗时:可能5 分钟就能上好几道菜。因为三个人同时在做不同的事。

但是,多人协作也会出问题

假设你和小王都想用同一个锅

  • 你正在炒菜,小王也想炒菜
  • 两个人抢一个锅,结果菜洒了,锅也坏了

这就是并发编程中的资源争抢问题。解决方法是什么?加锁——"这个锅我在用,你等一下"。这就是后续章节要讲的synchronizedLock

🔍 单线程的 3 大瓶颈

在现代计算机中,单线程程序有三个致命问题:

瓶颈问题描述生活类比影响
CPU 利用率低等待 I/O(网络/磁盘/数据库)时 CPU 空转厨师等外卖食材,站在厨房发呆资源浪费,处理速度慢
响应性差一个任务卡住,整个程序卡住厨师等食材,所有客人都饿着用户体验差,系统不可用
吞吐量低无法利用多核 CPU厨房有 4 个灶台,只用 1 个性能天花板低

代码演示:单线程的低效

public class SingleThreadDemo { public static void main(String[] args) { long start = System.currentTimeMillis(); // 模拟 3 个耗时任务(每个 1 秒) task("任务1", 1000); task("任务2", 1000); task("任务3", 1000); long end = System.currentTimeMillis(); System.out.println("总耗时: " + (end - start) + "ms"); } static void task(String name, long sleepTime) { try { System.out.println("开始执行: " + name); Thread.sleep(sleepTime); // 模拟 I/O 等待 System.out.println("完成: " + name); } catch (InterruptedException e) { e.printStackTrace(); } } }

预期输出

开始执行: 任务1 完成: 任务1 开始执行: 任务2 完成: 任务2 开始执行: 任务3 完成: 任务3 总耗时: 3000ms

问题:3 个任务串行执行,总耗时 = 任务1 + 任务2 + 任务3 =3 秒。在等待任务1完成时,CPU 是空闲的,但没人用它。

🔍 并发解决了什么问题

并发编程通过"同时处理多个任务"来解决单线程的瓶颈:

解决方案原理适用场景效果
多核并行计算多个 CPU 核心同时执行不同任务CPU 密集型(计算、加密)性能提升 N 倍(N=核心数)
异步处理主线程不等待,任务完成后回调I/O 密集型(网络、数据库)响应时间大幅降低
资源利用等待时让出 CPU 给其他任务混合场景系统整体吞吐量提升

代码演示:多线程并行处理

public class MultiThreadDemo { public static void main(String[] args) { long start = System.currentTimeMillis(); // 创建 3 个线程同时执行 Thread t1 = new Thread(() -> task("任务1", 1000)); Thread t2 = new Thread(() -> task("任务2", 1000)); Thread t3 = new Thread(() -> task("任务3", 1000)); t1.start(); t2.start(); t3.start(); try { t1.join(); // 等待 t1 完成 t2.join(); // 等待 t2 完成 t3.join(); // 等待 t3 完成 } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("总耗时: " + (end - start) + "ms"); } static void task(String name, long sleepTime) { try { System.out.println("开始执行: " + name); Thread.sleep(sleepTime); System.out.println("完成: " + name); } catch (InterruptedException e) { e.printStackTrace(); } } }

预期输出

开始执行: 任务1 开始执行: 任务2 开始执行: 任务3 完成: 任务1 完成: 任务2 完成: 任务3 总耗时: 1000ms

对比:多线程总耗时1 秒,是单线程的1/3。因为 3 个任务同时执行。

🔍 并发的演进

Java 并发编程经历了 20 多年的演进:

版本新增特性解决的问题
JDK 1.0Threadsynchronized基础多线程支持
JDK 5.0java.util.concurrent高效并发工具、线程池、并发集合
JDK 8CompletableFuture、Lambda异步编程简化
JDK 21虚拟线程、结构化并发轻量级线程、更好的并发控制

💻 可跑代码:3 个完整示例

示例 1:单线程处理(慢)

public class SlowServer { public static void main(String[] args) { System.out.println("=== 单线程处理 3 个请求 ==="); long start = System.currentTimeMillis(); // 模拟处理 3 个请求 handleRequest("用户A查询订单"); handleRequest("用户B提交支付"); handleRequest("用户C下载发票"); long end = System.currentTimeMillis(); System.out.println("总耗时: " + (end - start) + "ms"); System.out.println("用户平均等待: " + (end - start) / 3 + "ms"); } static void handleRequest(String request) { try { System.out.println("处理: " + request); Thread.sleep(1000); // 模拟网络 I/O System.out.println("完成: " + request); } catch (InterruptedException e) { e.printStackTrace(); } } }

预期输出

=== 单线程处理 3 个请求 === 处理: 用户A查询订单 完成: 用户A查询订单 处理: 用户B提交支付 完成: 用户B提交支付 处理: 用户C下载发票 完成: 用户C下载发票 总耗时: 3000ms 用户平均等待: 1000ms

示例 2:多线程并行处理(快)

public class FastServer { public static void main(String[] args) { System.out.println("=== 多线程处理 3 个请求 ==="); long start = System.currentTimeMillis(); // 创建 3 个线程并行处理 Thread t1 = new Thread(() -> handleRequest("用户A查询订单")); Thread t2 = new Thread(() -> handleRequest("用户B提交支付")); Thread t3 = new Thread(() -> handleRequest("用户C下载发票")); t1.start(); t2.start(); t3.start(); // 等待所有线程完成 try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("总耗时: " + (end - start) + "ms"); System.out.println("用户平均等待: " + (end - start) / 3 + "ms"); } static void handleRequest(String request) { try { System.out.println("处理: " + request); Thread.sleep(1000); System.out.println("完成: " + request); } catch (InterruptedException e) { e.printStackTrace(); } } }

预期输出

=== 多线程处理 3 个请求 === 处理: 用户A查询订单 处理: 用户B提交支付 处理: 用户C下载发票 完成: 用户A查询订单 完成: 用户B提交支付 完成: 用户C下载发票 总耗时: 1000ms 用户平均等待: 333ms

示例 3:线程不安全的反例(引出后续模块)

public class UnsafeCounter { private static int count = 0; public static void main(String[] args) throws InterruptedException { System.out.println("=== 线程不安全示例 ==="); // 两个线程各加 10000 次 Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { count++; // 这行不是原子操作! } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { count++; // 这行不是原子操作! } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("期望值: 20000"); System.out.println("实际值: " + count); System.out.println("差值: " + (20000 - count)); // 为什么不是 20000? // 因为 count++ 不是原子操作,多线程下会丢失更新 // 后续章节会详细讲解 CAS 和 synchronized } }

预期输出(每次运行可能不同):

=== 线程不安全示例=== 期望值: 20000 实际值: 15234 差值: 4766

问题分析count++看起来是一行代码,但实际上分三步:

  1. 读取 count 的值
  2. 加 1
  3. 写回 count

当两个线程同时执行时,可能读到相同的值,导致加 1 后覆盖对方的结果。这就是竞态条件

⚠️ 并发的代价(预告)

多人协作虽然快,但也会出问题:

问题描述生活类比后续章节
竞态条件多个线程同时修改共享数据,结果不可预期两个人同时改同一个文档02_原理与实现/04_锁体系总览
死锁两个线程互相等待对方释放锁,卡死两个人互相让路,结果都卡在门口03_问题与应对/03_并发问题排查实战
可见性问题一个线程修改了变量,另一个线程看不到一个人在黑板上写字,另一个人没看到02_原理与实现/03_并发内存模型
线程安全多线程下数据结构可能损坏多人同时写账本,账本可能乱套04_应用/07_并发集合详解

怎么解决?后面的篇目会一一拆解:

  • 锁机制(synchronized、ReentrantLock)
  • 原子类(AtomicInteger、CAS)
  • volatile 关键字
  • 并发集合(ConcurrentHashMap)
  • 线程池和同步器

// 竞态条件示例:两个人同时改同一个文档 public class RaceConditionExample { private static int balance = 100; public static void main(String[] args) throws InterruptedException { // 模拟两个线程同时扣款 Thread t1 = new Thread(() -> { int temp = balance; try { Thread.sleep(10); } catch (Exception e) {} balance = temp - 50; System.out.println("用户A扣款后余额: " + balance); }); Thread t2 = new Thread(() -> { int temp = balance; try { Thread.sleep(10); } catch (Exception e) {} balance = temp - 50; System.out.println("用户B扣款后余额: " + balance); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最终余额: " + balance); System.out.println("期望余额: 0, 实际: " + balance + " (应该为0)"); } }

🎯 学习路径

作为专栏入口,这里告诉你接下来的模块怎么走:

学习阶段核心内容你会收获
入门线程基础、内存模型、volatile、synchronized理解并发的基本概念,能写简单的线程安全代码
进阶锁机制、AQS、线程池、CompletableFuture看懂 JUC 源码,会设计并发方案
实战死锁排查、设计模式、虚拟线程能排查线上问题,能设计并发架构

💡 面试要点

  1. 为什么用多线程?

    • 提升性能:多核 CPU 并行计算
    • 提升响应:I/O 等待时 CPU 做其他事
    • 提升吞吐:同时处理更多请求
  2. 并发 vs 并行区别?

    • 并发:多个任务交替执行(单核也能做到)
    • 并行:多个任务同时执行(需要多核)
  3. 并发的代价?

    • 竞态条件:数据不一致
    • 死锁:线程互相等待
    • 可见性:修改不可见
    • 复杂度:代码更难写、更难测
  4. 单线程 vs 多线程性能对比?

    • CPU 密集型:多线程 ≈ 单线程(甚至更慢,因为线程切换开销)
    • I/O 密集型:多线程 >> 单线程(等待时做其他事)
  5. 如何避免线程安全问题?

    • 加锁:synchronized、ReentrantLock
    • 原子类:AtomicInteger、CAS
    • volatile:保证可见性
    • 并发集合:ConcurrentHashMap

📝 总结

要点记住这一句
动机单线程处理不过来,需要多线程并行
类比一个人做饭 vs 厨房多人协作
瓶颈CPU 空转、响应差、吞吐低
解决多核并行、异步处理、资源复用
代价竞态、死锁、可见性问题
演进Thread → JUC → 虚拟线程
下一步学线程基础,理解线程是什么

一句话总结:并发编程是为了解决"慢"的问题,但引入了"乱"的问题——后面的章节会教你如何"又快又稳"。