这份附录解决什么问题
正文 45 节回答的是「这个类为什么这样实现」;选型时问题反过来:手上这个场景,该用哪个类,代价是什么。附录 A 把本卷出现的类型按功能摊平,每个类一行,给一句话定位和对应章节,供写代码时查阅。
它不重复 目录与学习路径 里的三条读法,只做两件事:查类在哪、比较取舍。分类树用来建立全局印象,六张分类表用来定位,最后的选型表用来做决定。
核实口径:65 个类型逐个 javap
本卷出现的 java.util.concurrent 类型共 62 个,外加 java.lang.invoke.VarHandle、java.lang.ThreadLocal、java.lang.InheritableThreadLocal 3 个 java.base 类型,共 65 个。写这篇附录前,在本机 Temurin 25.0.5+8 上逐个跑过 javap -p,按六类加附属类统计:
export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH
# 类名从下面各表抄进 classes.txt,一行一个
while read -r c; do
javap -p "$c" >/dev/null 2>&1 || echo "MISSING $c"
done < classes.txt
locks total=14 ok=14 missing=0
containers total= 8 ok= 8 missing=0
queues total= 8 ok= 8 missing=0
atomics total=13 ok=13 missing=0
executors total=16 ok=16 missing=0
async total= 3 ok= 3 missing=0
adjunct total= 3 ok= 3 missing=0
循环没有输出任何 MISSING 行,合计 14+8+8+13+16+3+3 = 65。这说明表里的类名在本机全部存在。存在性确认不等于语义确认:字段名、方法签名、常量值仍以各节的 javap -p 与源码摘录为准。
分类树
下面这棵树按包与职责画,括号里是代表性实现。text 围栏里的箭头表示「接口到实现」或「父类到子类」,不表示依赖顺序。
java.util.concurrent
├── locks
│ ├── 接口 Lock / Condition / ReadWriteLock
│ ├── 同步器骨架 AbstractQueuedSynchronizer
│ │ AbstractQueuedLongSynchronizer(long state)
│ ├── 阻塞原语 LockSupport(park / unpark)
│ ├── 独占锁 ReentrantLock
│ ├── 读写锁 ReentrantReadWriteLock / StampedLock
│ └── 协作器 Semaphore / CountDownLatch / CyclicBarrier
│ Phaser / Exchanger
├── 并发容器
│ ├── 映射 ConcurrentMap → ConcurrentHashMap
│ │ ConcurrentSkipListMap(按 key 有序)
│ ├── 集合 ConcurrentSkipListSet / CopyOnWriteArraySet
│ ├── 列表 CopyOnWriteArrayList
│ └── 无锁队列 ConcurrentLinkedQueue / ConcurrentLinkedDeque
├── 阻塞队列
│ ├── 接口 BlockingQueue
│ ├── 数组有界 ArrayBlockingQueue
│ ├── 链表 LinkedBlockingQueue / LinkedTransferQueue
│ ├── 零容量 SynchronousQueue
│ ├── 优先级 PriorityBlockingQueue
│ └── 延迟 DelayQueue → Delayed
├── atomic
│ ├── 标量 AtomicInteger / AtomicLong / AtomicReference
│ ├── 带版本 AtomicStampedReference / AtomicMarkableReference
│ ├── 数组 AtomicIntegerArray / AtomicLongArray
│ ├── 字段更新器 AtomicIntegerFieldUpdater
│ │ AtomicReferenceFieldUpdater
│ └── 分片累加 Striped64 → LongAdder / LongAccumulator / DoubleAdder
├── 执行器
│ ├── 接口 Executor / ExecutorService
│ │ ScheduledExecutorService / CompletionStage
│ ├── 工厂 Executors / ThreadFactory
│ ├── 线程池 ThreadPoolExecutor
│ │ ScheduledThreadPoolExecutor
│ ├── 任务 Future / FutureTask / ForkJoinTask
│ │ RecursiveTask / RecursiveAction
│ ├── 工作窃取 ForkJoinPool / ForkJoinWorkerThread
│ └── 异步编排 CompletableFuture
└── 结构化并发 StructuredTaskScope(JDK 25 preview)
树外还有三个 java.base 类型:VarHandle 是原子访问与内存序的底座,ThreadLocal / InheritableThreadLocal 是线程封闭的载体。它们不属于 java.util.concurrent 包:VarHandle 在第 9 章展开,两个 ThreadLocal 在卷一讨论,这里一并核实。
锁与同步器
这一组回答「谁先拿到、拿不到怎么等、等的时候能不能被打断」。
| 类 | 一句话定位 | 本卷章节 |
|---|---|---|
Lock | 显式锁接口,把 lock / unlock 从语法块变成方法 | 2.1 |
Condition | 条件队列接口,await / signal 的契约 | 2.2 |
ReadWriteLock | 读写锁接口,读共享、写独占 | 3.1 |
AbstractQueuedSynchronizer | int state + CLH 变体队列,所有 AQS 同步器的骨架 | 1.1 |
AbstractQueuedLongSynchronizer | 同一套队列模型,state 换成 long | 3.1 |
LockSupport | park / unpark 原语,AQS 阻塞与唤醒的底座 | 1.2 |
ReentrantLock | 可重入独占锁,公平/非公平只差 initialTryLock 与 tryAcquire | 2.1 |
ReentrantReadWriteLock | 读写分离,写锁可降级为读锁,读锁不能升级 | 3.2 |
StampedLock | 版本戳 + 乐观读,不可重入,适合读多写少 | 3.3 |
Semaphore | 许可模型,共享模式最直接的应用 | 4.1 |
CountDownLatch | 一次性倒计数,数到 0 后所有等待者放行 | 4.2 |
CyclicBarrier | 可重用栅栏,内部是 ReentrantLock + Condition,不走 AQS | 4.2 |
Phaser | 分阶段同步,可动态注册与注销参与者 | 4.2 |
Exchanger | 两个线程在配对点交换对象,可带超时 | 4.3 |
ReentrantLock 与 ReentrantReadWriteLock 的 Sync 都继承 AQS;Semaphore 与 CountDownLatch 用共享模式;CyclicBarrier 是这一组里唯一不用 AQS 的,它直接用 ReentrantLock 和 Condition 实现代际(generation)切换。
并发容器
这一组回答「多个线程读写同一个结构时,读要不要加锁、写要锁多大范围」。
| 类 | 一句话定位 | 本卷章节 |
|---|---|---|
ConcurrentMap | 并发映射接口,定义 putIfAbsent / compute 等原子复合操作 | 5.2 |
ConcurrentHashMap | 桶数组 + 首节点 CAS + 扩容协助,读完全不加锁 | 5.1 |
ConcurrentLinkedQueue | 无锁 FIFO 队列,MAX_HOPS = 8 控制帮助删除的步数 | 10.1 |
ConcurrentLinkedDeque | 无锁双端队列,头尾都能进出 | 10.1 |
ConcurrentSkipListMap | 跳表实现的有序并发映射,无 HeadIndex,head 是 Index | 6.2 |
ConcurrentSkipListSet | 基于 ConcurrentSkipListMap 的有序集合 | 6.2 |
CopyOnWriteArrayList | 写时复制列表,写锁是内置 Object lock,迭代读旧数组 | 6.1 |
CopyOnWriteArraySet | 基于 CopyOnWriteArrayList 的集合,去重靠线性扫描 | 6.1 |
ConcurrentHashMap 与 CopyOnWriteArrayList 的取舍在 6.3 的矩阵里展开:前者适合「读多写少但写也要快」,后者适合「读极多、写极少、迭代要多」。两者的迭代器都不是强一致快照,只是弱一致或旧数组快照。
阻塞队列
这一组回答「生产者和消费者速度不匹配时,在哪里排队、排不下怎么办」。
| 类 | 一句话定位 | 本卷章节 |
|---|---|---|
BlockingQueue | 阻塞队列接口,put / take 可阻塞,offer / poll 可超时 | 7.1 |
ArrayBlockingQueue | 数组 + 单锁 + 两个条件,容量必填 | 7.1 |
LinkedBlockingQueue | 链表 + 双锁(putLock / takeLock),容量默认近似无界 | 7.1 |
SynchronousQueue | 零容量,生产者必须等到消费者接手 | 7.2 |
PriorityBlockingQueue | 无界优先级堆,元素必须可比较 | 7.2 |
DelayQueue | 无界延迟队列,只有到期元素才能被取走 | 7.2 |
Delayed | 延迟元素接口,getDelay 决定还有多久到期 | 7.2 |
LinkedTransferQueue | 链表无锁传输队列,transfer 会等到消费者接手 | 7.2 |
JDK 25 的 SynchronousQueue 内部是 Transferer<E> extends LinkedTransferQueue<E>,公平模式另有 qlock、waitingProducers、waitingConsumers 三个字段。旧书里的 TransferStack / TransferQueue 在本机 javap 找不到,附录末尾有记录。
原子类
这一组回答「不加锁怎么保证单变量更新的原子性,竞争高时怎么减少 CAS 失败」。
| 类 | 一句话定位 | 本卷章节 |
|---|---|---|
AtomicInteger | int 的 CAS 封装,JDK 25 仍走 jdk.internal.misc.Unsafe | 8.1 |
AtomicLong | long 的 CAS 封装,低竞争计数的默认选择 | 8.1 |
AtomicReference | 对象引用的 CAS 封装 | 8.1 |
AtomicStampedReference | 引用 + int 版本戳,用来识别 ABA | 10.2 |
AtomicMarkableReference | 引用 + boolean 标记,只关心「有没有动过」 | 10.2 |
AtomicIntegerArray | 数组元素的原子更新,索引参与寻址 | 9.3 |
AtomicLongArray | long 数组元素的原子更新 | 9.3 |
AtomicIntegerFieldUpdater | 用反射拿到字段偏移,原子更新已有类的 volatile int | 8.1 |
AtomicReferenceFieldUpdater | 同上,目标字段是对象引用 | 8.1 |
Striped64 | base + Cell[] 的分片累加骨架,包内可见 | 8.3 |
LongAdder | 高并发计数,sum() 是弱一致估算 | 8.2 |
LongAccumulator | 可自定义累加函数的分片累加器 | 8.2 |
DoubleAdder | double 版分片累加,语义与 LongAdder 相同 | 8.2 |
注意 AtomicInteger 的实现路径:javap -p -c 能看到 getAndAdd 调用 Unsafe.getAndAddInt,不是 VarHandle。第 9 章讲的是「新代码怎么用 VarHandle」,不是「JDK 内部已经全换了」。
执行器与任务
这一组回答「任务交给谁跑、线程怎么复用、异常怎么回到调用方」。
| 类 | 一句话定位 | 本卷章节 |
|---|---|---|
Executor | 只声明 execute(Runnable) 的最小型执行接口 | 11.1 |
ExecutorService | 增加 submit / shutdown / awaitTermination | 11.2 |
ScheduledExecutorService | 增加延迟与周期任务 | 11.3 |
AbstractExecutorService | 用 FutureTask 把 submit 搭在 execute 上的骨架 | 11.2 |
Executors | 线程池与队列的工厂方法,方便但容易掩盖容量问题 | 11.1 |
ThreadFactory | 线程创建策略,负责命名与守护属性 | 11.1 |
Future | 异步结果的读取、取消、超时接口 | 13.3 |
FutureTask | Future + Runnable 的最小实现,状态机只有七种 | 11.2 |
ThreadPoolExecutor | ctl 高 3 位状态、低 29 位线程数,Worker 继承 AQS | 11.1 |
ScheduledThreadPoolExecutor | 用 DelayedWorkQueue 堆 + leader 实现定时 | 11.3 |
ExecutorCompletionService | 按完成顺序取 Future,而不是按提交顺序 | 13.1 |
ForkJoinPool | 工作窃取池,ctl 是 long,WorkQueue 用 seqlock | 12.1 |
ForkJoinTask | 可 fork / join 的任务基类,轻量于普通线程 | 12.1 |
RecursiveTask | 有返回值的递归分治任务 | 12.3 |
RecursiveAction | 无返回值的递归分治任务 | 12.3 |
ForkJoinWorkerThread | 窃取池里的工作线程,绑定一个 WorkQueue | 12.2 |
异步编排与结构化并发
这一组只有三个类型,但第 13 章整章围绕第一个展开。
| 类 | 一句话定位 | 本卷章节 |
|---|---|---|
CompletableFuture | 依赖栈 + 完成回调,result 与 stack 两个 volatile 字段 | 13.1–13.3 |
CompletionStage | 异步阶段的组合契约,定义 thenApply / thenCompose 等 | 13.1 |
StructuredTaskScope | JDK 25 preview 的结构化并发作用域,open / fork / join | 13.2
(使用需 --enable-preview) |
StructuredTaskScope 在 JDK 25 仍是 preview,且 ShutdownOnFailure / ShutdownOnSuccess 已不存在;本机 javap 对这两个嵌套类报「找不到类」。写作与选型时只能把它当作「未来可能替代部分 CompletableFuture 扇出」的方向,不能当稳定 API 用。
选型表:六个维度
六个维度的含义先说清楚,否则表格只能背不能推:
- 有序性:迭代或取出的顺序是否可预测。
- 读写比例:结构更适合读多写少、读写均衡,还是写多。
- 迭代语义:迭代器看到的是弱一致视图、快照,还是根本不支持迭代。
- 容量语义:无界、有界、零容量、按优先级、按到期时间。
- 内存放大:每个元素相对裸值的额外对象与指针开销。
- 能否阻塞:生产/消费或加锁时,调用线程是否会被挂起。
| 类型 | 有序性 | 读写比例 | 迭代语义 | 容量语义 | 内存放大 | 能否阻塞 |
|---|---|---|---|---|---|---|
ConcurrentHashMap | 无序 | 读多写少 | 弱一致 | 无界(可给初始容量) | 中,每桶一个 Node | 否 |
ConcurrentSkipListMap | 按 key 排序 | 读多写少 | 弱一致 | 无界 | 高,多层索引 | 否 |
CopyOnWriteArrayList | 插入序 | 读极多写极少 | 旧数组快照 | 无界 | 高,每次写复制整个数组 | 否 |
ArrayBlockingQueue | FIFO | 读写均衡 | 弱一致 | 有界,必填 | 低,纯数组 | 是 |
LinkedBlockingQueue | FIFO | 读写均衡 | 弱一致 | 可有界,默认近似无界 | 高,每元素一个 Node | 是 |
SynchronousQueue | 无存储 | 交接型 | 不支持迭代 | 零容量 | 低 | 是 |
PriorityBlockingQueue | 按优先级 | 读多写少 | 弱一致 | 无界 | 中,数组堆 | 是(空时) |
DelayQueue | 按到期时间 | 读多写少 | 弱一致 | 无界 | 中,堆 + 延迟对象 | 是(无到期元素时) |
ReentrantLock | 不适用 | 不适用 | 不适用 | 不适用 | 低 | 是(lock) |
StampedLock | 不适用 | 读多写少 | 不适用 | 不适用 | 低 | 是(悲观读写锁);乐观读不阻塞 |
AtomicLong | 不适用 | 低竞争写 | 单值强一致 | 不适用 | 低 | 否 |
LongAdder | 不适用 | 高竞争写 | sum 弱一致 | 不适用 | 中,Cell[] + 缓存行填充 | 否 |
读这张表的方法:先看「有序性」和「容量语义」能不能满足业务约束,再看「读写比例」和「内存放大」决定值不值得。比如同样是有界 FIFO,ArrayBlockingQueue 与 LinkedBlockingQueue 的差别不在功能,而在锁的粒度和每元素的内存开销;同样是有序映射,ConcurrentSkipListMap 的排序能力要用内存和常数因子换。
如果不想逐列比,可以按下面四个问题走一遍,大多数场景三步内就能收敛:
这个结构需要在两个线程之间传递任务吗?
├── 不需要:只是共享读写
│ ├── 需要按 key 或到期时间排序?
│ │ ├── 是 → ConcurrentSkipListMap
│ │ └── 否 → ConcurrentHashMap
│ └── 写极少、遍历极多?
│ └── 是 → CopyOnWriteArrayList
└── 需要:生产者 / 消费者
├── 必须限制在途数量? → ArrayBlockingQueue
├── 必须手把手交接、不存储? → SynchronousQueue
├── 按到期时间取? → DelayQueue
├── 按优先级取? → PriorityBlockingQueue
└── 只是普通缓冲? → LinkedBlockingQueue(显式给容量)
四个问题分别对应「有没有跨线程传递」「要不要顺序」「写有多频繁」「容量怎么定义」。答完还定不下来,通常是需求本身没写清,而不是类选错。
落到借阅系统
把表压到 libraryd-conc 的具体位置,选型就不抽象了:
| 场景 | 选择 | 理由 | 章节 |
|---|---|---|---|
| 按 ISBN 查图书 | ConcurrentHashMap | 读多写少,单次 get 无锁 | 5.2 |
| 预约按到期时间排序 | ConcurrentSkipListMap | 需要按 dueAt 范围查询 | 6.2 |
| 通知订阅名单 | CopyOnWriteArrayList | 几乎不变,遍历远多于写 | 6.1 |
| 借书请求缓冲 | ArrayBlockingQueue | 必须限流,满了让生产者阻塞 | 7.1 |
| 还书事件日志 | LinkedBlockingQueue | 双锁,生产消费可并行 | 7.1 |
| 预约到期提醒 | DelayQueue | 只有到期元素才能出队 | 7.2 |
| 借阅次数统计 | LongAdder | 高并发写,允许 sum() 弱一致 | 8.2 |
| 单本库存扣减 | AtomicInteger 或 CHM.compute | 单变量 CAS;跨字段用 compute | 8.1 |
| 下游并发上限 | Semaphore | 许可数就是并发上限 | 4.1 |
| 启动预热 | CountDownLatch | 一次性等待所有预热完成 | 4.2 |
| 目录缓存乐观读 | StampedLock | 读多写少,版本戳校验 | 3.3 |
| 异步通知扇出 | CompletableFuture | 三个下游并行,统一收口 | 13.1 |
举一个容易选错的:借书请求缓冲不要用 LinkedBlockingQueue 的默认构造器。它默认容量是 Integer.MAX_VALUE,看起来「不会满」,实际是把背压消失了,高峰期请求会堆到 OOM。用 ArrayBlockingQueue(1024),满了让 put 阻塞,或者用 offer(timeout) 明确拒绝,才是 7.1 讲的容量语义。
本机 javap 找不到的类
为了不和旧版教材混淆,下面这些名字在本机 Temurin 25.0.5+8 上逐个 javap -p 过,全部报「找不到类」:
MISSING java.util.concurrent.ConcurrentSkipListMap$HeadIndex
MISSING java.util.concurrent.SynchronousQueue$TransferStack
MISSING java.util.concurrent.SynchronousQueue$TransferQueue
MISSING java.util.concurrent.StructuredTaskScope$ShutdownOnFailure
MISSING java.util.concurrent.StructuredTaskScope$ShutdownOnSuccess
对应的事实是:JDK 25 的 ConcurrentSkipListMap 用 Index<K,V> 做头,没有 HeadIndex;SynchronousQueue 的传输器继承 LinkedTransferQueue,没有独立的 TransferStack / TransferQueue;StructuredTaskScope 的关闭策略改成 Joiner,两个 ShutdownOn* 子类已移除。引用旧书时看到这五个名字,先当历史对照处理。
小结
这张类图的用法不是背,而是三步:先在分类树里定位职责,再在分类表里找到具体类与章节,最后用六维选型表比较代价。65 个类型都在本机 javap 核实过,5 个旧版名字确认不存在;选型结论落到借阅系统时,优先看容量语义与读写比例,再看内存放大。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。