# 《Java并发实现原理:JDK源码剖析》知识点全解 > AI生成|2026年10月28日|codebuddy|原书:余春龙《Java并发实现原理:JDK源码剖析》,电子工业出版社,2020-03-01 > **我的阅读**:进度 100%|累计 7小时55分钟|开始 2020-06-30|最近 2024-05-26|读完 2020-07-02 ## 一句话总结 把 `java.util.concurrent` 从"会用的 API"还原成"能推演的机器":先讲清 JMM、内存屏障、happen-before 这些底层规则,再沿着 Atomic → Lock → 同步工具 → 并发容器 → 线程池 → ForkJoinPool → CompletableFuture 的复杂度阶梯,逐层拆解每一层是如何用 CAS、volatile、队列和 park/unpark 搭出来的。 **适合谁**:有 1–3 年 Java 开发经验、写过多线程代码但说不清"为什么这么写才对"的工程师;准备面试深水区、需要讲清 AQS / CHM / 线程池细节的人;想读懂 JDK 源码但屡次在 CLH 队列、StampedLock、ForkJoinPool 处卡住的人。 **不适合**:零 Java 基础、没写过 `synchronized` 和 `Thread` 的初学者(会看不懂动机);只想找"并发工具怎么调 API"速查手册的人(本书重原理轻用法)。 **前置知识**:Java 语法与集合框架、`Thread`/`Runnable` 基本用法、操作系统里"锁/信号量/就绪队列"的粗浅概念、能看懂泛型与内部类。读第 7、8 章还建议先了解"双端队列"和"栈"这两种数据结构。 ## 一、全书地图 全书是一条"从规则到构件、从构件到框架"的单向依赖链,前一章是后一章的地基,跳读容易断链。 | 层次 | 章节 | 主题 | 在全书中的角色 | 依赖 | |---|---|---|---|---| | 规则层 | 第1章 多线程基础 | interrupt、synchronized、wait/notify、volatile、JMM 与 happen-before、内存屏障、final、无锁编程 | 定义"什么算正确"的判定标准,后面所有章节都在满足这套规则 | 无 | | 原子层 | 第2章 Atomic 类 | AtomicInteger/Long、AtomicBoolean/Reference、Stamped/Markable Reference、FieldUpdater、AtomicXxxArray、Striped64 与 LongAdder | 把"无锁"落到可用的整数/引用操作上,是后续所有 CAS 结构的最小零件 | 第1章(volatile + CAS 语义) | | 锁层 | 第3章 Lock 与 Condition | Lock/Condition、读写锁、Condition、StampedLock | 用 AQS 把"原子 + 队列 + 阻塞"组装成可重入锁与条件等待 | 第2章(CAS)、第1章(park 语义) | | 协作层 | 第4章 同步工具类 | Semaphore、CountDownLatch、CyclicBarrier、Exchanger、Phaser | 在 AQS 之上做"线程间协作协议",不管理共享数据只管理节奏 | 第3章(AQS 共享/独占模式) | | 容器层 | 第5章 并发容器 | BlockingQueue、BlockingDeque、CopyOnWrite、ConcurrentLinkedQueue/Deque、ConcurrentHashMap、ConcurrentSkipListMap/Set | 把锁与 CAS 用到数据结构内部,解决"容器自身的线程安全" | 第2、3章 | | 执行层 | 第6章 线程池与 Future | 线程池继承体系、ThreadPoolExecutor、Callable/Future、ScheduledThreadPoolExecutor、Executors | 解决"线程本身怎么复用、任务怎么排队、结果怎么取" | 第5章(阻塞队列) | | 调度层 | 第7章 ForkJoinPool | 核心数据结构、工作窃取队列、状态控制、阻塞唤醒、任务提交、窃取执行、fork/join、优雅关闭 | 分治任务的专用执行器,是线程池的特化与升级 | 第6章 | | 编排层 | 第8章 CompletableFuture | 用法、四种任务原型、CompletionStage、内部原理、DAG 执行、allOf 计算图 | 把"多个异步任务"编排成图,是执行层的组合语法 | 第6、7章 | 一条主线贯穿全书:**volatile 保证可见性 → CAS 保证原子性 → 队列保证排队 → park/unpark 保证不空转 → 组合出锁 → 组合出工具与容器 → 组合出执行器 → 组合出编排**。看到任何新类,都可以问它落在这条链的哪一环。 ## 二、核心知识点 ### 1. interrupt 是"协商信号",不是"强制终止" `interrupt()` 只做三件事:设置中断标志位;若线程阻塞在 `wait`/`sleep`/`join`/`park` 等可中断点,则唤醒它并抛出 `InterruptedException`,**同时清除中断标志**;若阻塞在不可中断的 I/O 或 `synchronized` 抢锁上,则什么也不发生。 为什么重要:Java 用"协作式中断"取代 `Thread.stop()`(会留下不一致的中间状态)。误区是以为调了 `interrupt()` 线程就停了,或以为捕获 `InterruptedException` 后标志还在。正确写法是捕获后 `Thread.currentThread().interrupt()` 恢复标志再向上抛,否则调用方失去感知。另一个高频误区:`isInterrupted()` 不清标志,`Thread.interrupted()` 会清。 ### 2. monitor 是 synchronized 与 wait/notify 的共同底座 `synchronized` 的互斥语义由对象头 Mark Word 承载:无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(OS 互斥量,涉及用户态/内核态切换),只能升级不能降级。JDK 6 引入这套优化后,它在低竞争下已不输 `ReentrantLock`。这也解释了"为什么锁对象不能是基本类型或 null、不建议锁 String 常量"(常量池共享会导致无关代码互锁)。误区:认为 `synchronized` 一定慢;认为锁的是代码块(锁的是 monitor 所属对象,`static` 方法锁的是 Class 对象)。补充:偏向锁在新版 JDK 已被移除(JDK 15 默认关闭、JDK 18 移除),读源码注意版本差异,具体版本行为**待核对**。 `wait()` 则是同一个 monitor 的另一面,它做三件事:释放 monitor、进入该对象的等待队列、被唤醒后重新竞争 monitor 才能返回。所以 `wait/notify` 必须写在 `synchronized` 块内(否则抛 `IllegalMonitorStateException`),且 `notify()` **不会立刻释放锁**,要等同步块结束。 必须用 `while` 而非 `if` 包住 `wait`:被唤醒不代表条件成立(虚假唤醒、多个等待者抢同一条件)。`notifyAll` 与 `notify` 的取舍本质是"能否确定只有一个等待者满足条件"。这把"等待队列"与"同步队列"区分开——正是第 3 章 `Condition` 要抽象出来的东西。 ### 3. volatile 管可见性与有序性,唯独不管原子性 写 volatile 变量时,JIT 会在写之后插入 `StoreLoad` 屏障,把写缓冲刷回主存并使其他核的缓存行失效;读 volatile 时禁止其后的读写被重排到读之前。所以 volatile 建立的是"写 → 读"的 happen-before 边。 为什么重要:它是所有 CAS 结构和 AQS 里 `state` 字段的基础。典型误区是拿 volatile 做计数器(`i++` 是"读—改—写"三步,中间仍可插入),正解是 `AtomicInteger` 或 `LongAdder`。另一个误区是以为 volatile 引用能让对象内部字段也安全发布——只有引用本身安全,对象内部字段仍需 final 或额外同步。 ### 4. happen-before 是判断"要不要加同步"的唯一尺子 JMM 定义 8 条规则,常用的有:程序次序(同线程内前面的语句先于后面的)、监视器锁(解锁先于后面对同一锁的加锁)、volatile(写先于读)、线程启动(`start()` 先于该线程内任何动作)、线程终止(线程内所有动作先于别的线程从 `join()` 返回)、中断(`interrupt()` 先于被中断线程检测到中断)、对象终结(构造完成先于 `finalize()`)、传递性。 用法:不要问"这段代码线程安全吗",要问"这两个操作之间有没有 happen-before 边"。没有边且至少一方是写,就是数据竞争。误区:把 happen-before 理解成"时间上的先后"——它是**可见性保证**,不是执行顺序保证,A 先于 B 可见不代表 A 先被执行。 ### 5. 内存屏障是 JMM 落到 CPU 的"翻译层" 四类屏障:`LoadLoad`(禁止上面的读与下面的读重排)、`StoreStore`(禁止上面的写与下面的写重排)、`LoadStore`、`StoreLoad`(最重,禁止读写互换,通常也是唯一需要显式指令的)。x86 的 TSO 模型天然保证前三种,只对 `StoreLoad` 需要 `mfence`/`lock` 前缀。 为什么重要:它解释了"为什么 volatile 在 x86 上几乎零成本,在 ARM 上代价明显"。误区:以为屏障是 Java 层面的方法调用(`Unsafe` 里的 `loadFence()` 等只是给编译器与 CPU 的提示);以为加了屏障就等于加锁(屏障不互斥、不排队)。 ### 6. final 的"构造安全"靠的是禁止重排序 `final` 字段的写必须在构造函数返回前完成,且 JMM 禁止"把对象引用的赋值重排到 final 字段写之前"。这条规则让"正确构造后发布的对象"无需额外同步即可被看见。 但它有前提:构造函数中不能把 `this` 逸出(注册监听器、放进静态集合、开新线程传 `this`)。一旦逸出,别的线程可能看到 final 字段为 0/null。这是最隐蔽的一类并发 bug,也是单例"双重检查锁"必须给实例字段加 `volatile` 的原因。 ### 7. 无锁编程 = CAS 循环 + 失败重试 CAS 对应 `Unsafe.compareAndSwapXxx`,语义是"若当前值等于期望值则更新,否则返回失败",由 CPU 的 `cmpxchg` 指令保证原子。无锁算法的模板固定:读旧值 → 基于旧值算新值 → CAS,失败则重读重试。 代价有三:自旋消耗 CPU(高竞争下不如阻塞)、只能保证单个变量的原子性(多变量要么打包成一个对象引用,要么用锁)、以及 **ABA 问题**(值从 A 变 B 又变回 A,CAS 看不出中间发生过变化,栈/链表结构会因此损坏)。ABA 的解法是加版本号,即 `AtomicStampedReference`。 ### 8. Atomic 家族与 LongAdder:从"单点 CAS"到"热点分散" - `AtomicInteger/AtomicLong`:`volatile value` + CAS,提供 `getAndIncrement`、`compareAndSet` 等。 - `AtomicBoolean/AtomicReference`:同构,把 int 换成 boolean/引用。 - `AtomicStampedReference`(值 + int 版本戳)/`AtomicMarkableReference`(值 + boolean 标记):解决 ABA,内部用 `Pair` 包装,CAS 的是 Pair 引用。 - `AtomicXxxFieldUpdater`:用反射取字段偏移量,对**已有类的 volatile 字段**做原子更新,省去对象包装。前提是字段必须 `volatile`、非 `private`、不能是 `static`。 - `AtomicXxxArray`:元素级 CAS,元素按 `volatile` 语义访问。 `AtomicLong` 的瓶颈在于所有线程 CAS 同一个 `value`,失败率随核数上升。`Striped64` 的思路是**分散热点**:`base` 加一个 `Cell[]`,线程按探针哈希落到不同 Cell 各自累加,求和时把 base 与所有 Cell 相加。关键设计有三:Cell 用 `@sun.misc.Contended` 填充避免伪共享;数组按 2 的幂扩容,上限约为 CPU 核数;`cellsBusy` 用 CAS 保证初始化与扩容互斥。代价是 `sum()` 不是原子快照(遍历中可能有并发修改),适合"高频写、低频读统计",不适合需要强一致的序号生成。 ### 9. AQS 是整章的"发动机":volatile state + CLH 变体队列 + LockSupport `AbstractQueuedSynchronizer` 只做三件事:用一个 `volatile int state` 表示同步状态;用一条双向链表(CLH 队列的变体)存放阻塞线程,节点 `Node` 带 `waitStatus`(SIGNAL/CANCELLED/CONDITION/PROPAGATE);用 `LockSupport.park/unpark` 挂起与唤醒。 子类只需实现 `tryAcquire/tryRelease`(独占)或 `tryAcquireShared/tryReleaseShared`(共享),把"能不能拿到"的判定交给业务语义,排队、阻塞、唤醒全部由 AQS 复用。`ReentrantLock` 的公平/非公平差异只体现在 `tryAcquire` 里"先 `hasQueuedPredecessors()` 还是先 CAS"。 `Condition` 是 AQS 内部的 `ConditionObject`:每个 Condition 一条单向条件队列,`await()` 释放全部锁并把自己挪到条件队列,`signal()` 把节点从条件队列转移到同步队列。这解释了"一个锁可以有多个等待集"(生产者/消费者各一个 Condition,避免 `notifyAll` 的惊群)。 ### 10. 读写锁靠"位切割"共享一个 state,StampedLock 靠"戳"避免写饥饿 `ReentrantReadWriteLock` 把 32 位 `state` 切成高 16 位读计数、低 16 位写计数,从而一个 int 同时表达两种锁。规则:读读共享、读写互斥、写写互斥;**写锁可以降级为读锁**(持有写锁时再取读锁,然后释放写锁),**读锁不能升级为写锁**(会死锁)。它支持公平与非公平,非公平模式下读锁可能长期插队导致写饥饿。 `StampedLock` 用"版本戳"取代锁对象:`tryOptimisticRead()` 返回一个 stamp,读完后 `validate(stamp)` 校验期间有没有写发生,没发生就免锁读成功。它**不可重入**、**不支持 Condition**、**不区分读写线程**,但乐观读几乎没有开销,适合读极多写极少的场景。误区:把 `StampedLock` 当 `ReentrantReadWriteLock` 的平替——它在持锁期间调用其他会阻塞的方法会导致 stamp 失效甚至死锁。 ### 11. 同步工具类:本质上都是"带计数的 AQS" | 工具 | 语义 | 内部要点 | 一次性? | |---|---|---|---| | Semaphore | 许可证计数,`acquire` 减、`release` 加 | 共享模式 AQS,`state` 即剩余许可 | 否 | | CountDownLatch | 等 N 个事件完成 | `state` 初始化为 count,`countDown` 递减到 0 释放所有等待者 | 是,不能重置 | | CyclicBarrier | N 个线程互相等到齐再一起走 | 基于 `ReentrantLock` + `Condition`,用 `Generation` 支持循环复用 | 否,可复用 | | Exchanger | 两个线程在屏障点交换数据 | 单槽 `Slot` + 链表 `Node` 配对,先到者等待 | 否 | | Phaser | 多阶段、可动态增减参与者的屏障 | `phase/parties/unarrived` 打包进一个 long,CAS 推进阶段 | 否,可分阶段 | 选型口诀:**等事件用 CountDownLatch,等人齐用 CyclicBarrier,限流用 Semaphore,两两交换用 Exchanger,多阶段流水线用 Phaser**。常见误区:CountDownLatch 复用(第二次 `await` 直接返回);CyclicBarrier 的 `await` 超时后屏障被打破(Broken),后续线程抛 `BrokenBarrierException`;Semaphore 的 `release` 不校验持有者,可以被"凭空"调用。 ### 12. 并发容器:不同数据结构选不同同步策略 - **BlockingQueue**:`ArrayBlockingQueue` 用**一把锁 + 两个 Condition**(notEmpty/notFull);`LinkedBlockingQueue` 用**两把锁**(putLock/takeLock)+ `AtomicInteger count`,入队出队可并行,吞吐更高但计数是弱一致;`SynchronousQueue` 不存储元素,直接配对交接;`PriorityBlockingQueue` 用堆 + 单锁;`DelayQueue` 用优先队列按到期时间排序。 - **CopyOnWriteArrayList/Set**:写时复制整个数组,读完全无锁。适合"读多写极少、元素少"(监听器列表),写操作 O(n) 且内存翻倍,迭代器是弱一致快照,**迭代中不会抛 ConcurrentModificationException,但也看不到新元素**。 - **ConcurrentLinkedQueue/Deque**:Michael-Scott 队列,CAS 更新 `head`/`tail`,且允许指针"滞后"(tail 不总指向真正的尾节点,要跳两步才更新),这是为了减少 CAS 次数。 - **ConcurrentHashMap**:JDK 8 起放弃 Segment 分段锁,改为 `Node[]` + 链表 + 红黑树;空桶用 CAS 直接放入,非空桶 `synchronized` 锁住桶的头节点;`size` 用 `baseCount + CounterCell[]`(Striped64 思路);扩容时多线程通过 `helpTransfer` 协助迁移,用 `ForwardingNode` 标记已迁移的桶;链表长度 ≥ 8 且容量 ≥ 64 时树化。 - **ConcurrentSkipListMap/Set**:用跳表替代红黑树,读操作无锁、写用 CAS,天然支持有序与范围查询,是并发版的 `TreeMap`。 误区集中区:CHM 的 `size()` 是估算值;CHM 不允许 null 键值(无法区分"不存在"与"值为 null");`Collections.synchronizedMap` 与 CHM 的差别是"整表锁"与"桶级锁";COW 容器不适合频繁写。 ### 13. 线程池:ctl 一个 int 装下"状态 + 线程数" `ThreadPoolExecutor` 用 `AtomicInteger ctl` 承载两个字段:高 3 位是运行状态(RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED),低 29 位是工作线程数。这个设计让"状态迁移 + 线程计数"能在一次 CAS 里完成。 `execute()` 的三段式:核心线程未满 → 新建核心线程;核心满 → 入队;队列满 → 未到 maximum 则建非核心线程;再满 → 执行拒绝策略(Abort/CallerRuns/Discard/DiscardOldest)。`Worker` 继承 AQS 并用一个不可重入的独占锁标记"正在执行任务",`shutdown` 时据此判断能否中断。 必须记住的两个坑:**`LinkedBlockingQueue` 无界会导致 maximumPoolSize 永久失效**(队列永远不满,永远走不到建非核心线程那步);**核心线程默认不超时**,只有 `allowCoreThreadTimeOut(true)` 才会回收。`ScheduledThreadPoolExecutor` 用 `DelayedWorkQueue`(堆)替代阻塞队列,`scheduleAtFixedRate` 与 `scheduleWithFixedDelay` 的区别是"按开始时间算周期"还是"按结束时间算间隔"。 ### 14. ForkJoinPool:每个线程一条双端队列,偷的人从另一端拿 分治任务的特点是"父任务 fork 出子任务后要等子任务"。若用普通线程池,父线程阻塞会浪费一个线程。ForkJoinPool 的方案是:每个 Worker 有自己的 `WorkQueue` 双端队列,自己 fork 的任务压入自己队列(LIFO,符合深度优先、局部性好),空闲线程从**别人队列的另一端**(FIFO)窃取,减少竞争。 队列用数组实现,`base` 与 `top` 两个指针,窃取用 CAS 抢 `base`。奇偶槽位有讲究:外部提交的任务与 fork 出的任务分列不同槽位,以便区分处理。`ForkJoinTask` 内部有 `status` 字段记录完成状态,`join` 时若任务未完成,会先帮助执行(work-stealing 的"帮忙"语义)再 `park`,避免真正的阻塞。`ForkJoinPool` 用 `ctl` 同时编码运行状态、活跃线程数 AC 与总线程数 TC,`scanState` 用于控制唤醒与扩容。 误区:以为 ForkJoinPool 适合所有任务(它适合**可分解、子任务粒度均匀、无阻塞 I/O** 的任务);以为 `fork` 就是立刻开新线程(只是入队);在 `compute()` 里做阻塞调用会拖垮整个池。 ### 15. CompletableFuture:把回调组织成 DAG 内部只有一个 `result` 字段,要么是结果值,要么是 `AltResult`(包装异常或 null)。每个 `CompletableFuture` 上挂一个 `Completion` 栈(Treiber 栈,CAS 压栈),任务完成时 `postComplete()` 依次弹出并触发依赖,形成"完成 → 通知下游"的链式反应。 四种任务原型覆盖所有用法:`AsyncSupply`(有返回值 + 线程池)、`AsyncRun`(无返回值 + 线程池)、`Supply`(有返回值、同步)、`Run`(无返回值、同步)。带 `Async` 后缀且不传 Executor 时用 `ForkJoinPool.commonPool()`。 `thenApply`/`thenCompose`/`thenCombine` 的区别是"映射 / 扁平化 / 合并"。`allOf` 不是简单的计数等待,而是把 N 个依赖两两组合成一棵 `BiRelay` 二叉树,逐层向上汇聚,从而把"等待 N 个"降到 O(log N) 的链深度。误区:以为 `Async` 一定换线程(在同一线程池且已饱和时可能仍在当前线程执行);忘记异常处理(`exceptionally`/`handle`/`whenComplete` 三者的返回值语义不同);在 `commonPool` 里跑阻塞任务。 ## 三、关键概念速查表 | 概念 | 一句话解释 | 用在哪儿 | |---|---|---| | happen-before | 两个操作间的可见性偏序关系,有边才敢说"能看到" | 判断任何一段代码是否需要同步 | | 内存屏障 | 禁止特定读写重排的 CPU/JIT 指令 | volatile、final、CAS 的底层实现 | | CAS | 比较并交换,CPU 单指令保证原子 | 所有原子类、AQS、CHM、无锁队列 | | ABA 问题 | 值改回原值导致 CAS 误判"没变过" | 无锁栈/链表,用版本戳解决 | | 伪共享 | 两个无关变量落在同一缓存行,互相使失效 | LongAdder 的 Cell 填充 | | AQS | state + CLH 队列 + park/unpark 的同步框架 | ReentrantLock、信号量、Latch、线程池 Worker | | 条件队列 | 等待某条件成立的线程集合,与同步队列分离 | Condition、wait/notify、阻塞队列的 notEmpty | | 独占/共享模式 | 同一时刻允许一个还是多个线程通过 | 锁 vs 信号量/读写锁读锁 | | 锁降级 | 持写锁时再取读锁,再放写锁 | 缓存"先改后读"的一致性 | | 乐观读 | 先读再验戳,验证失败才加锁 | StampedLock 读多写少场景 | | 工作窃取 | 空闲线程从别人队列另一端取任务 | ForkJoinPool、并行流 | | ctl | 一个 int 同时编码状态与线程数 | ThreadPoolExecutor、ForkJoinPool | | 写时复制 | 写时整体复制,读无锁 | CopyOnWriteArrayList、监听器列表 | | 树化 | 链表过长时转红黑树,查询 O(n)→O(log n) | ConcurrentHashMap 桶内结构 | | Completion 栈 | 每个 future 上的回调栈,完成时逐个触发 | CompletableFuture 的依赖传播 | ## 四、可复用的方法/框架 **判断"要不要加同步"的四步** 1. 找出所有被多个线程访问的可变状态(包括"看起来只读"的集合)。 2. 对每一对"一写一读"检查是否存在 happen-before 边(同一把锁?volatile?start/join?)。 3. 没有边 → 要么加边(锁/volatile/原子类),要么改成不可变。 4. 加完锁再问:锁的粒度是否覆盖了复合操作(check-then-act 必须整体在锁内)。 **选并发工具的决策树** - 只是单个计数器/标志位 → `AtomicXxx`;高并发统计 → `LongAdder`。 - 需要条件等待 / 多等待集 → `ReentrantLock` + `Condition`。 - 读远多于写 → `StampedLock` 乐观读或 `CopyOnWrite`。 - 只是协调线程节奏 → 第 4 章工具类,不要自己写 `wait/notify`。 - 共享数据 → 并发容器,不要给 `HashMap` 外面套 `synchronized`。 - 任务执行 → 线程池;任务可递归分解 → ForkJoinPool;多任务编排 → CompletableFuture。 **读 JDK 并发源码的通用套路** 1. 先找"状态字段"(`state`/`ctl`/`status`/`result`),它通常编码了全部关键信息。 2. 再找"状态是怎么被原子修改的"(CAS 还是锁)。 3. 再找"等待者放在哪"(同步队列、条件队列、Treiber 栈、WorkQueue)。 4. 最后找"谁负责唤醒"(unpark、signal、postComplete、signalWork)。 任何并发类都能被这四个问题拆开。 **性能判断的三条经验** - 竞争程度决定策略:低竞争自旋/偏向锁更好,高竞争排队阻塞更好。 - 临界区越短越好,但锁的粒度不是越细越好(细到失去原子性就是 bug)。 - 任何"看起来免费"的优化(乐观读、COW、无锁)都在别处付了代价(重试、内存、复杂度)。 ## 五、我的阅读轨迹 | 项目 | 数据 | |---|---| | 进度 | 100% | | 累计阅读 | 7小时55分钟 | | 开始阅读 | 2020-06-30 | | 最近阅读 | 2024-05-26 | | 读完时间 | 2020-07-02 | 全书已读完,但"读完"与"读透"之间有明显落差:2020-07-02 首轮通读只用了约两天,属于"跟着目录过一遍"的强度;2024-05-26 的最近一次阅读说明这本书被当作长期参考书反复回查,而不是一次性读物。 **复习建议(按遗忘曲线排优先级)** | 优先级 | 章节 | 理由 | 建议动作 | |---|---|---|---| | 高 | 第1章 JMM 与 happen-before、内存屏障 | 后面七章全部建立在此,规则记不牢则源码读不懂 | 闭卷默写 8 条规则与 4 类屏障,再对照 | | 高 | 第3章 AQS | 是 ReentrantLock、信号量、Latch、线程池 Worker 的公共底座 | 手写一个不可重入的独占锁,只看 `state` 与队列 | | 高 | 第5章 ConcurrentHashMap | 细节最多、面试与实战命中率最高 | 画出 put 的完整分支图(空桶/链表/树/扩容/协助迁移) | | 中 | 第6章 ThreadPoolExecutor | 参数组合的坑比原理更容易踩 | 复盘一次线上池配置,检查队列是否有界 | | 中 | 第2章 Striped64 / LongAdder | 伪共享与分段思想可迁移到其他热点计数 | 用一个计数器做压测对比 AtomicLong | | 中 | 第8章 CompletableFuture | 用法易学、DAG 原理易忘 | 手画 `allOf` 的 BiRelay 二叉树 | | 低 | 第7章 ForkJoinPool | 场景较窄,但工作窃取思想值得理解 | 只需记住双端队列 + 奇偶槽位 + join 帮忙 | | 低 | 第4章 工具类 | API 层面易查,原理同 AQS | 记选型口诀即可 | ## 六、行动清单 1. 闭卷默写 happen-before 的 8 条规则和 4 类内存屏障,写不出的条目回读第 1 章。 2. 用 `Unsafe`/`VarHandle` 之外的手段手写一个基于 `volatile state` + `LockSupport` 的简易独占锁,再对照 AQS 的 `acquire/release` 找差距。 3. 给 `AtomicLong` 与 `LongAdder` 写同一份多线程自增压测,记录吞吐差异并解释原因。 4. 画出 `ConcurrentHashMap.put` 的完整流程图,覆盖空桶 CAS、桶内链表、树化、`helpTransfer`、`ForwardingNode` 五个分支。 5. 审计现有项目里所有 `ThreadPoolExecutor` 的构造参数,检查是否存在无界队列导致 `maximumPoolSize` 失效。 6. 把项目里一处 `wait/notify` 或 `if + wait` 改写成 `while + wait` 或 `Condition.await`,并补上中断恢复。 7. 用 `CompletableFuture` 重写一段串行的多步远程调用,画出改造后的 DAG 并标出异常分支。 8. 为本文档的速查表补充一列"我在哪个项目里用过",把概念锚定到真实场景。 ## 七、延伸阅读 1. **《Java并发编程实战》(Brian Goetz 等)**——本书的"设计动机"补充。它讲"为什么要这样设计、什么场景该用什么",但源码细节少;先读它建立正确的心智模型,再读余春龙这本补实现,顺序是"先道后术"。若已读完本书,可反向重读它的第 2–5 章,会有"原来这句结论对应那段源码"的顿悟。 2. **《深入理解Java虚拟机》(周志明)**——本书第 1 章的 JMM、内存屏障、锁优化在 JVM 层面的放大版。本书讲"Java 内存模型规定什么",它讲"HotSpot 怎么实现、对象头长什么样、锁升级在源码里怎么走"。建议与本书第 1 章交叉阅读。 3. **《操作系统导论》(Remzi Arpaci-Dusseau)**——本书所有并发原语的"物理底座":锁、条件变量、信号量、死锁、无锁数据结构。它用伪代码而非 Java 讲,能帮你把 AQS 的 CLH 队列还原成"就是一条等待队列"。适合作为前置或并行读物,读完再看 ForkJoinPool 的窃取算法会轻松很多。 阅读顺序建议:**《操作系统导论》(打底)→《Java并发编程实战》(建立判断力)→ 本书(打通源码)→《深入理解Java虚拟机》(下沉到 JVM)**。