第 15 章 · 并发进阶:JMM、锁与异步编排
本章目标:理解 JMM(Java 内存模型) 的可见性与有序性问题;掌握 volatile 的适用边界;能用 ReentrantLock/Condition 替代 synchronized 实现复杂协作;熟练使用 CountDownLatch、Semaphore 等并发工具类;理解 CAS 与原子类;用 CompletableFuture 编排异步流水线;认识 JDK 21 虚拟线程;能通过 jstack 排查死锁;对照 python-dev 的 GIL 与 asyncio。
学时建议:6~7 小时(含 2.5 小时实验)
前置:完成 ch10 多线程与并发基础;建议已学 ch13 泛型、ch14 设计模式。
15.1 场景说明:批量任务的线上事故
toolkit-demo 上线后做批量对账:8 个线程并发累加计数器,结果每天总数都对不上;加 synchronized 后性能掉了 70%。ch10 你学会了"让代码跑起来",本章学会"跑对、跑快、能排查":
| 现象 | 根因层 |
|---|---|
| 结果偶尔错误 | 可见性 / 原子性(JMM) |
| 性能不升反降 | 锁竞争 / 上下文切换 |
| 程序假死 | 死锁 / 活锁 |
| 异步回调地狱 | 缺少编排工具(CompletableFuture) |
说明:本章实验均为本地教学虚构场景,压测数据仅示意,不代表真实生产指标。
15.2 JMM:线程看到的世界不一样
CPU 缓存与编译器优化让「每个线程一份工作内存副本」,JMM 规定变量何时从主内存同步到工作内存:
线程 A ── 工作内存 A ─┐
├─ 主内存(堆中的共享变量)
线程 B ── 工作内存 B ─┘
| 问题 | 含义 | 典型后果 |
|---|---|---|
| 可见性 | A 改了,B 看不到 | 循环退出标志失效,线程死循环 |
| 原子性 | count++ 是读-改-写三步 | 并发下丢失更新 |
| 有序性 | 指令可能被重排 | 双重检查锁拿到"半成品"对象 |
happens-before 是 JMM 的承诺:unlock 先于后续 lock、volatile 写先于后续读、线程 start() 先于其动作、join() 后于其结束。记住这条链,比背规则有用。
15.3 volatile:只保证"看得见"
public class ShutdownFlag {
private volatile boolean running = true; // 去掉 volatile 试一下:可能永不退出
public void runLoop() {
while (running) {
// 工作中……
}
System.out.println("已停止");
}
public void stop() {
running = false;
}
}
| 能做 | 不能做 |
|---|---|
| 保证可见性(写立即刷主存) | 不保证原子性:volatile int count; count++ 仍丢更新 |
| 禁止指令重排(建立屏障) | 不适合复合操作(check-then-act) |
适用:状态标志位、双重检查锁中的实例字段。不适用:计数器。
15.4 synchronized vs ReentrantLock
ch10 已用 synchronized。ReentrantLock 提供更强的控制力:
| 能力 | synchronized | ReentrantLock |
|---|---|---|
| 加锁/释放 | 自动(块结束) | 手动 lock()/unlock(),必须 finally |
| 尝试锁 | ❌ | tryLock(timeout) 避免死等 |
| 可中断等待 | ❌ | lockInterruptibly() |
| 公平性 | 非公平 | 可选公平锁 |
| 条件变量 | 单一 wait/notify | 多个 Condition |
import java.util.concurrent.locks.ReentrantLock;
public class StockCounter {
private final ReentrantLock lock = new ReentrantLock();
private int stock;
public boolean tryDecrease(long waitMillis) throws InterruptedException {
if (!lock.tryLock(waitMillis, java.util.concurrent.TimeUnit.MILLISECONDS)) {
return false; // 拿不到锁快速失败,而不是死等
}
try {
if (stock <= 0) {
return false;
}
stock--;
return true;
} finally {
lock.unlock(); // 永远放在 finally
}
}
}
15.5 Condition:精细的等待/通知
经典生产者-消费者:队列空时消费者等 notEmpty,满时生产者等 notFull——两个条件分开等,比 notifyAll 唤醒所有人高效:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<T> {
private final int capacity;
private final Queue<T> queue;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) {
this.capacity = capacity;
this.queue = new LinkedList<>();
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 满了就等"不满"
}
queue.offer(item);
notEmpty.signal(); // 通知"有货了"
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
T item = queue.poll();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
工程上多数场景直接用 ch10 的 BlockingQueue(其内部正是 Lock + Condition 实现);本节目的是看得懂源码。
15.6 并发工具三剑客
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
| 工具 | 场景 | 一句话 |
|---|---|---|
CountDownLatch | 主线程等 N 个子任务完成 | 倒计时门闩,一次性 |
CyclicBarrier | N 个线程互相等到齐再走 | 可循环的集合点 |
Semaphore | 限制同时访问的并发数 | 许可证发放机 |
// 对账场景:等 8 个分片任务全部完成再汇总
CountDownLatch latch = new CountDownLatch(8);
for (int shard = 0; shard < 8; shard++) {
int s = shard;
pool.submit(() -> {
try {
reconcileShard(s);
} finally {
latch.countDown();
}
});
}
latch.await(); // 阻塞直到计数归零
mergeResults();
// 限流:最多 10 个线程同时调用下游
Semaphore permits = new Semaphore(10);
permits.acquire();
try {
callDownstream();
} finally {
permits.release();
}