下载工作台
Java 编程实战

并发进阶:JMM、锁与异步编排

试读上半部分 · 解锁后可读全文

第 15 章 · 并发进阶:JMM、锁与异步编排

本章目标:理解 JMM(Java 内存模型) 的可见性与有序性问题;掌握 volatile 的适用边界;能用 ReentrantLock/Condition 替代 synchronized 实现复杂协作;熟练使用 CountDownLatchSemaphore 等并发工具类;理解 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 先于后续 lockvolatile 写先于后续读、线程 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 已用 synchronizedReentrantLock 提供更强的控制力:

能力synchronizedReentrantLock
加锁/释放自动(块结束)手动 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 个子任务完成倒计时门闩,一次性
CyclicBarrierN 个线程互相等到齐再走可循环的集合点
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();
}

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。