为什么需要并发编程
📋 概述
一句话:并发编程是为了解决"单线程处理不过来"的问题——让程序能同时做多件事,提升性能和响应速度。
从一个真实场景说起
想象一下:你开了一家网店,平时一天卖 100 单,用一个客服处理订单,绰绰有余。
突然双十一来了,同时涌入1000 个订单。如果你还坚持"一个客服处理完一个再处理下一个",会发生什么?
- 第一个用户等了 10 分钟才拿到确认
- 第十个用户等了 100 分钟,直接退款走人
- 第一百个用户等到页面超时,再也不来了
问题的本质:单线程(单客服)的处理能力是有限的,当请求量超过处理速度时,系统就会崩溃。
解决方案:多雇几个客服(多线程),同时处理多个订单。这就是并发编程的动机。
💡 生活类比:厨房做饭
单线程:一个人做饭
想象你一个人在厨房做饭,流程是这样的:
总耗时:5 + 3 + 5 + 1 =14 分钟。在这 14 分钟里,你一直在忙,但效率很低——洗菜时灶台是空的,切菜时水龙头是空的。
多线程:厨房多人协作
现在雇了三个人:你负责切菜,小王负责洗菜,小李负责炒菜。三个人分工合作:
总耗时:可能5 分钟就能上好几道菜。因为三个人同时在做不同的事。
但是,多人协作也会出问题
假设你和小王都想用同一个锅:
- 你正在炒菜,小王也想炒菜
- 两个人抢一个锅,结果菜洒了,锅也坏了
这就是并发编程中的资源争抢问题。解决方法是什么?加锁——"这个锅我在用,你等一下"。这就是后续章节要讲的synchronized和Lock。
🔍 单线程的 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.0 | Thread、synchronized | 基础多线程支持 |
| JDK 5.0 | java.util.concurrent包 | 高效并发工具、线程池、并发集合 |
| JDK 8 | CompletableFuture、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++看起来是一行代码,但实际上分三步:
- 读取 count 的值
- 加 1
- 写回 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 源码,会设计并发方案 |
| 实战 | 死锁排查、设计模式、虚拟线程 | 能排查线上问题,能设计并发架构 |
💡 面试要点
为什么用多线程?
- 提升性能:多核 CPU 并行计算
- 提升响应:I/O 等待时 CPU 做其他事
- 提升吞吐:同时处理更多请求
并发 vs 并行区别?
- 并发:多个任务交替执行(单核也能做到)
- 并行:多个任务同时执行(需要多核)
并发的代价?
- 竞态条件:数据不一致
- 死锁:线程互相等待
- 可见性:修改不可见
- 复杂度:代码更难写、更难测
单线程 vs 多线程性能对比?
- CPU 密集型:多线程 ≈ 单线程(甚至更慢,因为线程切换开销)
- I/O 密集型:多线程 >> 单线程(等待时做其他事)
如何避免线程安全问题?
- 加锁:synchronized、ReentrantLock
- 原子类:AtomicInteger、CAS
- volatile:保证可见性
- 并发集合:ConcurrentHashMap
📝 总结
| 要点 | 记住这一句 |
|---|---|
| 动机 | 单线程处理不过来,需要多线程并行 |
| 类比 | 一个人做饭 vs 厨房多人协作 |
| 瓶颈 | CPU 空转、响应差、吞吐低 |
| 解决 | 多核并行、异步处理、资源复用 |
| 代价 | 竞态、死锁、可见性问题 |
| 演进 | Thread → JUC → 虚拟线程 |
| 下一步 | 学线程基础,理解线程是什么 |
一句话总结:并发编程是为了解决"慢"的问题,但引入了"乱"的问题——后面的章节会教你如何"又快又稳"。