并发编程全景概览
为什么需要并发?一句话:CPU 太快,I/O 太慢,单线程无法榨干硬件性能。一个典型的 Web 请求,CPU 计算只占 5%,其余 95% 都在等网络和磁盘。如果用单线程串行处理,CPU 大部分时间在"发呆"。并发编程的目标,就是让 CPU 在等待 I/O 时去做别的事情。
但并发不是免费的午餐。它带来三个根本性问题:可见性、原子性、有序性。Java 的整套并发体系——从 synchronized 到 java.util.concurrent——都在围绕这三个问题构建解决方案。
进程、线程与协程
进程(Process)
进程是操作系统资源分配的基本单位。每个进程有独立的内存空间(代码段、数据段、堆、栈),进程间通信需要通过 IPC(管道、消息队列、共享内存、Socket 等)。
进程 A 进程 B
┌──────────────┐ ┌──────────────┐
│ 代码段 │ │ 代码段 │
│ 数据段 │ │ 数据段 │
│ 堆 │ │ 堆 │
│ 栈 │ │ 栈 │
└──────────────┘ └──────────────┘
↕ IPC(管道/Socket/共享内存)↕
进程间隔离性好,一个进程崩溃不影响另一个。但创建和切换开销大(涉及页表切换、TLB 刷新)。
线程(Thread)
线程是 CPU 调度的基本单位。同一进程内的多个线程共享堆和方法区,但各自有独立的栈和程序计数器。
进程
┌────────────────────────────────────┐
│ 共享区域:堆 + 方法区 │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │线程A │ │线程B │ │线程C │ │
│ │ 栈 │ │ 栈 │ │ 栈 │ │
│ │ PC │ │ PC │ │ PC │ │
│ └─────┘ └─────┘ └─────┘ │
└────────────────────────────────────┘
Java 线程模型:在主流 JVM(HotSpot)中,Java 线程与操作系统线程是 1:1 映射。new Thread().start() 底层会调用 pthread_create(Linux)或 CreateThread(Windows)创建一个内核线程。这意味着:
- 线程创建/销毁有系统调用开销
- 线程切换涉及内核态/用户态切换
- 线程数量受操作系统限制(通常几千到几万)
协程(Virtual Thread / Coroutine)
JDK 21 引入的**虚拟线程(Virtual Thread)**本质上是用户态的协程。它由 JVM 调度而非操作系统调度,创建成本极低(约 1KB 栈空间 vs 平台线程约 1MB),可以轻松创建百万级虚拟线程。
// JDK 21+ 虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return "done";
});
}
}
虚拟线程适合 I/O 密集型 场景(如 Web 服务器),但对 CPU 密集型任务没有优势——因为最终还是要映射到有限的平台线程上执行计算。
三者对比
| 特性 | 进程 | 平台线程 | 虚拟线程 |
|---|---|---|---|
| 调度者 | 操作系统 | 操作系统 | JVM |
| 内存隔离 | 完全隔离 | 共享堆 | 共享堆 |
| 创建开销 | 重(MB 级) | 中(~1MB 栈) | 轻(~1KB 栈) |
| 切换开销 | 重(页表切换) | 中(内核态切换) | 轻(用户态切换) |
| 数量上限 | 数百 | 数千~数万 | 数百万 |
| 适用场景 | 隔离性要求高 | 通用 | I/O 密集型 |
并发的三个核心问题
所有并发 Bug 的根源都可以追溯到三个问题:
可见性(Visibility)
一个线程对共享变量的修改,另一个线程不一定能立即看到。
根源在硬件:现代 CPU 有多级缓存(L1/L2/L3),每个核心操作的是自己缓存中的副本。线程 A 修改了变量 x,这个修改可能还停留在 A 所在核心的 L1 Cache 中,线程 B 从自己的缓存读到的仍是旧值。
线程 A (CPU 0) 线程 B (CPU 1)
│ │
写 x=1 读 x=?
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ L1 Cache│ │ L1 Cache│
│ x = 1 │ │ x = 0 │ ← 还没同步!
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌──────────────────────────┐
│ 主内存 x = 0 │ ← 还没回写!
└──────────────────────────┘
Java 的解法:volatile(保证可见性)、synchronized(释放锁时刷新缓存)、final(构造完成后可见)。
原子性(Atomicity)
一个操作在执行过程中不会被线程调度器中断。
经典例子:count++ 看似一条语句,实际上是三个操作——读取、加 1、写回。两个线程同时执行 count++,可能出现丢失更新:
初始值 count = 0
线程 A: 读取 count = 0
线程 B: 读取 count = 0
线程 A: count + 1 = 1
线程 B: count + 1 = 1
线程 A: 写回 count = 1
线程 B: 写回 count = 1
结果:count = 1(期望 2)
Java 的解法:synchronized(互斥锁)、Lock(显式锁)、AtomicXxx(CAS 无锁原子操作)。
有序性(Ordering)
程序执行的顺序不一定与代码编写的顺序一致。
编译器和 CPU 会对指令进行重排序以优化性能。在单线程下不会有问题(as-if-serial 语义),但在多线程下可能导致意外行为。
经典反例——双重检查锁的单例模式(不使用 volatile 时):
// 错误示范:没有 volatile 修饰
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在这里!
}
}
}
return instance;
}
instance = new Singleton() 在字节码层面分为三步:
- 分配内存空间
- 调用构造方法初始化
- 将引用赋值给
instance
CPU 可能重排序为 1→3→2,导致线程 B 在第一次检查时看到 instance != null,但拿到的是一个尚未初始化完成的对象。
Java 的解法:volatile(禁止重排序)、synchronized(同步块内的操作不会被重排序到同步块外)、happens-before 规则。
JUC 全景图
java.util.concurrent 包(简称 JUC)是 Doug Lea 设计的并发工具包,从 JDK 5 开始引入。它的设计哲学是:把并发编程中反复出现的模式抽象为可复用的组件。
java.util.concurrent 全景图
├── 锁与同步
│ ├── synchronized(JVM 内置)
│ ├── ReentrantLock(可重入互斥锁)
│ ├── ReentrantReadWriteLock(读写锁)
│ ├── StampedLock(乐观读锁,JDK 8+)
│ └── AQS(AbstractQueuedSynchronizer,锁的基础框架)
│
├── 原子操作
│ ├── AtomicInteger / AtomicLong / AtomicBoolean
│ ├── AtomicReference / AtomicStampedReference
│ ├── AtomicIntegerArray / AtomicLongArray
│ └── LongAdder / LongAccumulator(高并发计数器,JDK 8+)
│
├── 并发容器
│ ├── ConcurrentHashMap(分段锁 / CAS + synchronized)
│ ├── CopyOnWriteArrayList(写时复制)
│ ├── ConcurrentLinkedQueue(无锁队列)
│ └── BlockingQueue 家族
│ ├── ArrayBlockingQueue(有界阻塞队列)
│ ├── LinkedBlockingQueue(可选有界)
│ ├── PriorityBlockingQueue(优先级队列)
│ ├── SynchronousQueue(无容量,直接传递)
│ └── DelayQueue(延迟队列)
│
├── 线程池
│ ├── ThreadPoolExecutor(核心实现)
│ ├── ScheduledThreadPoolExecutor(定时任务)
│ ├── ForkJoinPool(分治并行,JDK 7+)
│ └── Executors(工厂方法,不推荐直接使用)
│
├── 同步工具
│ ├── CountDownLatch(倒计数门闩)
│ ├── CyclicBarrier(可重用栅栏)
│ ├── Semaphore(信号量)
│ ├── Exchanger(线程间交换数据)
│ └── Phaser(灵活的阶段同步,JDK 7+)
│
├── 异步编程
│ ├── Future / FutureTask
│ └── CompletableFuture(异步编排,JDK 8+)
│
└── 线程本地
└── ThreadLocal / InheritableThreadLocal
学习路线建议
理解这个体系有一条清晰的依赖链:
线程基础与生命周期
│
▼
JMM(可见性 / 有序性 / happens-before)
│
▼
synchronized + volatile(最基本的同步原语)
│
▼
CAS + Atomic(无锁编程基础)
│
▼
AQS(锁框架的核心抽象)
│
├──► Lock 体系(ReentrantLock, ReadWriteLock)
│
├──► 同步工具(CountDownLatch, Semaphore 等)
│
▼
线程池(Executor 框架)
│
▼
CompletableFuture(异步编排)
│
▼
并发容器(ConcurrentHashMap, BlockingQueue 等)
本目录的后续文章将沿这条路线展开,每篇文章的知识前置条件在文首标注。
并发模型:共享内存 vs 消息传递
并发编程有两种基本模型:
| 共享内存模型 | 消息传递模型 | |
|---|---|---|
| 通信方式 | 线程读写共享变量 | 线程间发送/接收消息 |
| 同步方式 | 显式加锁 | 隐式(消息的发送先于接收) |
| 代表语言 | Java、C++ | Erlang、Go(CSP) |
| Java 中的体现 | synchronized、volatile |
BlockingQueue、Exchanger |
Java 主要采用共享内存模型,通过 JMM 规范来定义线程间的可见性和有序性。但 JUC 中的 BlockingQueue 等组件也提供了消息传递风格的编程方式——生产者和消费者通过队列通信,避免直接操作共享状态。
线程安全的三个层次
不是所有代码都需要加锁。线程安全可以分为三个层次,从强到弱:
1. 不可变(Immutable)
对象一旦创建就不能修改,天然线程安全。
// String 是不可变的,天然线程安全
String s = "hello";
// 使用 Collections.unmodifiableList 创建不可变集合
List<String> list = Collections.unmodifiableList(Arrays.asList("a", "b"));
// JDK 9+ 工厂方法
List<String> list2 = List.of("a", "b");
2. 线程安全(Thread-Safe)
对象自身内部实现了正确的同步,调用方无需额外加锁。
// ConcurrentHashMap 内部实现了细粒度的锁,调用方无需同步
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
// AtomicInteger 基于 CAS 实现原子操作
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
3. 非线程安全(Not Thread-Safe)
对象自身没有同步机制,需要调用方自行保证线程安全。
// HashMap 非线程安全,多线程需要外部同步
HashMap<String, Integer> map = new HashMap<>();
// 多线程操作需要加锁或使用 ConcurrentHashMap
架构要点:被问到"如何保证线程安全"时,不要只说加锁。正确的回答路径是:
- 先考虑能否设计为不可变对象
- 再考虑能否用线程封闭(ThreadLocal、栈封闭)
- 然后考虑无锁方案(CAS、Atomic 类)
- 最后才考虑加锁(synchronized、Lock)
核心底层问题导航
| 问题 | 涉及章节 |
|---|---|
| 进程和线程的区别? | 本文 |
| 并发和并行的区别? | 本文 |
| Java 线程和操作系统线程的关系? | 本文 |
| 并发编程的三大核心问题是什么? | 本文 |
| synchronized 和 volatile 的区别? | 03-synchronized-volatile.md |
| ReentrantLock 和 synchronized 的区别? | 04-lock-aqs.md |
| AQS 的原理? | 04-lock-aqs.md |
| 线程池的核心参数? | 05-thread-pool.md |
| CountDownLatch 和 CyclicBarrier 的区别? | 06-concurrent-utils.md |
| ThreadLocal 原理及内存泄漏? | 07-threadlocal.md |