JUC

并发的问题空间(Why 并发工具存在)

所有 Java 并发工具,本质上都在解决有限计算资源在多执行主体之间的分配与协调问题

竞态源于三条件并存——状态被共享、状态可变、访问无序并发。消解任一条即安全:不共享不可变、或给并发访问强加秩序(协调)。

该问题空间按来源分为三层,层次越靠上越接近并发的本体:

层次 问题域 核心命题
本体问题(任何并发模型固有) 竞争安全 共享状态的变更必须不可分割地完成;本质冲突:写-写、读-写
本体问题(任何并发模型固有) 协作同步 多个线程需要在时间或阶段上达成一致——不是竞争,而是协作
基底问题(共享内存模型引入) 可见性与有序性 一个线程的修改,何时、以何种顺序被其他线程观察到;源于 JMM,消息传递模型中由通信本身承载
工程派生(资源有限性) 准入控制 控制同时访问者数量;互斥(N=1)与限流(N>1)是同一问题的两个参数点
工程派生(线程模型 + 隐式参数) 上下文隔离与传播 并发执行下,如何安全地保存、传递线程上下文
工程派生(任务生命周期) 取消与关闭 执行中的任务如何可被安全地停止,而非强制终结

JUC 同步工具的构建分层(How JUC 求解问题空间)

本文档讨论的同步工具,均可映射回上述问题域之一或其组合;其构建呈四层结构:

并发问题空间(三层六域,见上节)
   ↓
并发抽象模型 ─┬─ 准入控制器:互斥锁(N=1)、信号量(N>1)
              ├─ 协作同步器:门闩、栅栏、会合点
              └─ 上下文容器:ThreadLocal 系(落地为线程内私有 Map,不经由以下两层)
   ↓(准入与协作两支)
构建路径 ─┬─ 直接扩展 AQS:ReentrantLock / Semaphore / CountDownLatch
          ├─ 组合 AQS 产物:CyclicBarrier(Lock + Condition + 分代)、ArrayBlockingQueue
          └─ 绕开框架:StampedLock / Phaser / Exchanger
   ↓
公共原语层:CAS + LockSupport.park/unpark

两个横切域内嵌于所有工具:

稳定性 原因
问题空间 极稳定 并发的本体与基底问题,跨语言不变
抽象模型 稳定 准入 / 协作 / 上下文是跨平台通用抽象
构建路径 最易变 JDK 内部实现:AQS 随版本多次调整(Unsafe→VarHandle、节点结构重写),工具仍在增补(StampedLock 为 JDK 8 新增)
公共原语 极稳定 直接映射硬件原子指令与 OS 调度原语,数十年不变

AQS:Java 并发的“内核抽象”

问题驱动:每个阻塞型同步器都需要同一套基础设施——原子状态、等待队列、阻塞/唤醒,而无锁等待队列的实现难度远高于工具自身的语义。AQS 把这套共性下沉为统一框架,JUC 主要同步工具因此共享同一个“内核”。

问题域覆盖:state 的原子变更求解竞争安全;准入判定与排队求解准入控制;共享模式与条件队列承载协作同步;中断契约横切取消与关闭——三域一横切,对应构建分层图的"直接扩展"路径。

AQS 的设计哲学

AQS(AbstractQueuedSynchronizer)并不是“锁”,而是:

同步器不变部分的统一框架:状态托管、排队、阻塞/唤醒由框架固化,准入语义由子类填入

两条原则构成其哲学:

  1. **不变 / 可变分离**:任何阻塞型同步器 = 概念稳定的骨架(状态、队列、阻塞/唤醒)+ 随工具而异的语义(谁能通过)。AQS 以模板方法切分二者——子类只实现 tryAcquire / tryRelease(及 Shared 变体)的准入判定,调度、排队、阻塞统一托管。
  2. **成本只向竞争者收取**:无竞争走快路径,一次 CAS 即过;排队设施懒初始化,不竞争则不存在。慢路径的全部复杂度,只由竞争失败者承担(机制落地见自举约束)。

AQS 的核心模型:一份状态、两类队列、两种模式

state(volatile int)→ 资源状态机:准入判定的依据,语义由子类定义
同步队列(CLH 变体) → 安置竞争失败线程:排队、阻塞(park)、按序唤醒(unpark)
条件队列             → 安置协作型等待:await 入队,signal 转移至同步队列重新竞争

同一骨架按唤醒分发分两种模式,排队与阻塞完全复用:

模式 准入判定 唤醒分发 典型工具
独占 成 / 败 单点唤醒 ReentrantLock、写锁
共享 返回剩余量 按余量传播唤醒 读锁、Semaphore、CountDownLatch

state 只是一个数字,语义由子类解释(重入计数 / 许可数 / 倒数计数)——这是众多工具能共享同一框架的直接原因。

AQS 的机制设计:三个必答问题与一条自举约束

同步器的三个必答问题

剥离 Java 语境,任何阻塞型同步设施(OS 等待队列、Linux futex、分布式锁)都必须回答三个问题:

必答问题 设计空间 AQS 的取舍 取舍理由
谁能通过(准入判定) 状态如何表达、如何原子变更 volatile state + CAS,判定规则下放子类 框架不预设资源语义,换取通用性
没通过的怎么办(等待安置) 自旋(耗 CPU、零唤醒延迟)↔ 阻塞(让出 CPU、有唤醒延迟) 短暂自旋后 park 的混合策略 临界区时长未知,取两端折中
等待者何时再试(唤醒分发) 广播惊群 ↔ 精准定点 FIFO 队列 + 前驱唤醒后继 惊群浪费调度;排队天然回答"下一个是谁"

这组取舍与 Linux futex 同构:快路径用原子指令,慢路径才进等待队列。AQS 可视为该通用同步架构在用户态、库层面的实例化。

一条自举约束,统摄所有实现细节

AQS 面临一个根本约束:锁的实现内部不能再用锁(否则无限递归),等待队列本身必须是无锁结构(CAS + volatile):

AQS 的能力边界:acquire/release 范式

AQS 的抽象范式是 acquire/release——一切能表达为"获取 / 释放某种数量的资源"的同步语义。工具与该范式的匹配度,决定了它的构建层次:

构建层次 方式 代表 因果
直接扩展 内部 Sync 类继承 AQS ReentrantLock、ReadWriteLock、Semaphore、CountDownLatch 语义可归约为 acquire/release
组合复用 用 AQS 的产物拼装 CyclicBarrier(= Lock + Condition + 分代)、ArrayBlockingQueue 需要的是"锁内协作",不必发明新同步器
绕开框架 直接基于 CAS + park StampedLock(乐观读验证)、Phaser(动态阶段推进)、Exchanger(成对会合) 语义无法映射到 acquire/release 排队范式

统一规约:API 各异,契约同一

归约一:所有同步器都是 acquire/release 双操作的具名化。 JUC 刻意不定义统一接口,名字服务于领域语义,契约完全同源:

工具 acquire 形态 release 形态
Lock lock() unlock()
Semaphore acquire() release()
CountDownLatch await() countDown()
FutureTask get() 任务完成

归约二:每个阻塞型 acquire 都遵循同一组变体契约——本质是把失败处理的预算交给调用方:

变体 语义 调用方的预算
acquire(阻塞) 等到为止 无预算约束
tryAcquire 立即返回成败 快速失败 / 降级路径
tryAcquire(timeout) 有界等待 明确的延迟预算
acquireInterruptibly 等待中可被取消 外部取消权

工具 API 中超出该契约的方法,才是它真正的增量语义(如 StampedLock 的 validate、Phaser 的 register)

互斥锁:ReentrantLock 与 synchronized

问题域定位:互斥锁是竞争安全的排他解,同时是准入控制的 N=1 特例。Java 提供同一抽象的两个实现:JVM 内建的 synchronized 与 JDK 库层的 ReentrantLock。

抽象模型:一个骨架,两个基底

互斥锁的抽象骨架与实现基底无关:持有者 + 重入计数 + 等待队列。两个实现是该骨架在不同层的实例:

骨架要素 synchronized(ObjectMonitor,JVM/C++) ReentrantLock(AQS,JDK/Java)
持有者 + 重入计数 owner + recursions exclusiveOwnerThread + state
同步队列 EntryList CLH 队列
条件队列 WaitSet(单个) ConditionObject(可多个)

骨架之外,设计通则同样跨基底成立:

同一骨架与同一组通则在 JVM 层与 JDK 层各实现一次:骨架与通则属于概念稳定层,基底只是工程选址——再次印证构建分层的 U 形结论。

演进注脚:偏向锁曾为“单线程反复加锁”场景连 CAS 也省去,JDK 15 起废弃——多核竞争成为常态后,偏向撤销的成本反超收益。优化的生命周期取决于其环境假设的存续

根本分歧:语言结构 vs 库对象

两个实现的全部 API 差异,都是一个根本选择的投影——锁是语法块,还是普通对象

维度 synchronized(语言结构) ReentrantLock(库对象)
锁释放 块退出自动释放(含异常路径) 手动,依赖 try/finally 纪律
acquire 变体 仅无限阻塞一种形态 完整四变体(阻塞 / try / 限时 / 可中断)
条件等待集 单等待集(wait/notify) 多 Condition
公平性 不可配置(非公平) 可配置
可优化性 锁边界对编译器可见:锁消除、锁粗化 JIT 视角是普通方法调用,无语义级优化

因果链:语法块 → 锁边界对编译器与 JVM 可见 → 释放可自动、优化可施加,但表达力被语法封顶(无法跨作用域、无法定制策略);普通对象 → 表达力完整(变体、多条件队列、跨方法持锁、策略可配),但正确性退回为调用方纪律。

这组"安全换表达力"的交换在工程中反复出现:GC vs 手动内存管理、声明式事务 vs 编程式事务——语言/框架接管得越多,越安全也越不自由。

公平性:ReentrantLock 独有的增量

synchronized 无公平选项(固定非公平),公平策略是 ReentrantLock 在骨架之上唯一的语义增量;机制上只是准入判定时是否检查队列前驱,一行判断分出两种价值取向:

策略 价值取向 代价
公平锁 顺序正义 吞吐下降
非公平锁 系统效率 局部饥饿

公平性不是技术问题,而是系统价值判断。

非公平锁吞吐更高的机制因果:锁释放瞬间,新到线程可直接 CAS 抢锁,省去"唤醒队首 → 调度延迟 → 锁空闲窗口"的代价。公平性买的是顺序,付出的是上下文切换;故 ReentrantLock 默认非公平,公平模式只用于顺序敏感场景(如连接池分配)。

选型判据

能用 synchronized,就不要用 Lock。

优先 synchronized 的理由源于"锁是语法块"一侧的收益:自动释放消灭一类泄漏错误、JVM 可持续优化、不依赖手动纪律。升级到 Lock 的触发条件:需要 acquire 变体(试、限时、可中断)、多条件等待集、公平性,或非块结构持锁(跨方法 / 跨作用域)——任一命中即为"不能用 synchronized"。

读写分离:ReentrantReadWriteLock 与 StampedLock

问题域定位:仍属竞争安全 + 准入控制,但准入规则从计数细化为按操作类型的兼容性矩阵。问题空间已指明本质冲突只有写-写、读-写——读-读本无冲突,互斥锁的 N=1 准入把它也串行化了;读写分离即把准入规则对齐到真实冲突结构:

兼容性
共享 互斥
互斥 互斥

ReentrantReadWriteLock:悲观范式

StampedLock:乐观范式

这是从“锁竞争”向“冲突检测”的范式迁移,与数据库乐观并发控制(version 字段提交校验)、CAS 重试同构:版本号 + 事后校验替代进入时排他

选型判据

场景 选择 原因
需要重入或条件等待 ReentrantReadWriteLock StampedLock 缺这两项语义
读临界区短、状态可拷贝为局部变量 StampedLock 乐观读 读零持有,写不被阻塞
写占比高 退回互斥锁 兼容性矩阵的收益与读占比成正比,写多时分离开销倒挂

协作而非竞争:CountDownLatch、CyclicBarrier 与 Phaser

问题域定位:本章工具求解协作同步域——多个线程在时间或阶段上达成一致。它们与准入域工具共享 acquire/release 契约,分野在 state 的语义:不再表示资源量,而表示进度——await 不消耗配额,条件满足时全体放行(共享模式的"按余量传播"在此退化为广播)。等待的是事件,不是资源。

CountDownLatch:一次性门闩

CyclicBarrier:可循环栅栏

失败语义:协作与竞争的深层差异

竞争域的失败是隔离的——一个线程获取锁失败不影响他人;协作域的失败是连带的——互等结构中个体缺席必然波及集体。两个工具代表两种处理策略:

策略 工具 行为 工程含义
不检测,责任交使用方 CountDownLatch 计数缺位则等待者悬挂 必须 finally 中 countDown,或 await 带超时兜底
检测并传播 CyclicBarrier 一人中断/超时即 broken,全组抛 BrokenBarrierException 框架接管失败一致性,使用方处理集体重试

两者的本质区别

维度 CountDownLatch CyclicBarrier
对称性 等待者与计数者分离 参与者互等
可复用 否(state 单调至终态) 是(分代重置)
失败语义 无内建检测 broken 传播全组
构建路径 直接扩展 AQS(共享模式) 组合复用(Lock + Condition + 分代)

演进:Phaser 解除两个硬限制

硬限制 受限工具 Phaser 的解除方式
一次性 CountDownLatch 多阶段推进:phase 递增,天然循环
参与方固定 CyclicBarrier register / arriveAndDeregister 动态注册

代价:语义无法映射到 acquire/release 排队范式,只能绕开 AQS 自建(见能力边界)——灵活性的获得以失去框架托管为交换。

选型判据:等待者是否参与计数——外部观察者等内部事件完成 → CountDownLatch;对等参与者互等 → CyclicBarrier;多轮迭代 → CyclicBarrier;参与方动态增减 → Phaser。协作三件套的第三件"会合点"(Exchanger,成对数据交换)见后文。

准入控制的一般形式:Semaphore

问题域定位:互斥锁是准入控制的 N=1 特例,Semaphore 是其一般形式——以 N 份许可控制同时访问者数量。构建上经 AQS 共享模式直接扩展:state 即许可数,acquire 减、release 加,按余量传播唤醒。

抽象模型:配额,而非锁

锁与信号量的机制分界在所有权

维度 Semaphore
持有者 有(owner 记录) 无(只有计数)
重入 可定义(身份绑定计数) 无此概念
释放者约束 仅持有者可释放 任意线程可释放,甚至先释放后获取

边界警告:问题层面"互斥 = 准入 N=1",但工具层面 Semaphore(1) ≠ 互斥锁——差异即所有权。无所有权是双刃:跨线程释放成为可能(A 线程获取、B 线程归还,异步与管道场景的刚需),误释放也无任何防护。问题层等价、工具层不等价,再次印证"问题 ≠ 解法"。

增量语义(超出 acquire/release 契约的部分):批量许可 acquire(n) / release(n)、跨线程释放、许可数动态调整(reducePermits)。公平性选项与互斥锁同框架,但价值取向不同——限流场景下公平性防的是饥饿而非顺序。

工程意义:失效模式的转换

失败模式:忘记 release 导致许可泄漏,比锁泄漏更隐蔽——锁泄漏立即死锁,是响亮失败;许可泄漏是池子慢性萎缩,无声衰减。无所有权使框架无法代为检查,release 必须落在 finally。

会合点:Exchanger

问题域定位:协作同步域的第三件工具——两方对齐 + 双向数据交换,会合与通信合一,即 rendezvous 语义

与栅栏的区分:CyclicBarrier 是 N 方对齐、不带数据;Exchanger 是 2 方会合、各自携带数据并交换。协作章的对称性框架延续:双方互为计数者与等待者。

横向同构:Go 无缓冲 channel 的收发、Ada 的 rendezvous——"会合即通信"是跨语言的稳定模式;Exchanger 是 Java 中最接近同步 channel 的原语。

公共原语:LockSupport 与 park/unpark

问题域定位:构建分层的最底层。一切阻塞型工具最终落到两个原语——CAS(原子状态变更,由硬件原子指令支撑,见基础概念)与 park/unpark(线程阻塞 / 唤醒,本章)。

许可模型:丢失唤醒的消解

park/unpark 围绕二值许可(0/1,不累积)工作:unpark 发放许可,park 消耗许可,有许可则立即返回。因此 unpark 可先于 park 到达而不丢失——"检查条件 → 阻塞"之间被唤醒的竞态窗口被许可吸收。这是它取代 wait/notify 成为底层原语的根本原因。

对 wait/notify 的三重解放

约束 wait/notify park/unpark
锁依赖 必须先持有监视器锁 无需任何锁
时序依赖 notify 早于 wait 即丢失 unpark 先行被许可记住
唤醒目标 无法指定线程 精准定点:unpark(thread)

park 的返回契约

park 返回有三种原因:unpark、中断、虚假返回——且 park 不报告原因,调用方必须循环重检条件。AQS 自举约束中的中断契约("不响应但不吞掉")正是建立在这一契约之上。

OS 映射

每个线程挂一个 Parker(互斥量 + 条件变量,Linux 上经 futex)——U 形表"公共原语直接映射 OS 调度原语"的具体落点:park/unpark 是 OS 阻塞原语的 JVM 封装,CAS 是硬件 cmpxchg 的封装。上层全部工具的阻塞语义到此为止,再往下即内核调度。

上下文隔离与传播:ThreadLocal 及其演进

问题域定位:到此,文档第一次踏出"协调"主线。前面每件工具都承认共享、管理争用;ThreadLocal 走另一条根本路径——不共享:以"每线程一份副本"取消共享这个前提,争用从源头不存在。这正是它在构建分层图中唯一"不经由 CAS+park 原语层"的原因——无争用可仲裁,便不需要原子原语。本质是以空间换免同步

抽象模型:每线程副本

ThreadLocal 解决隐式参数传递——无需在调用链层层透传的上下文(事务、用户身份、traceId),挂在"当前线程"这个隐式坐标上。

关键设计:ThreadLocalMap 是 Thread 的字段(threadLocals),不是 ThreadLocal 的字段,以 ThreadLocal 实例为 key、值为 value。一个推论值千金——线程访问的永远是自己 Thread 上的 map,天然零竞争,免同步由此而来。

机制:一个设计选择,三个后果

map-on-Thread 这一选择同时派生隔离收益与两个风险,三者同源:

后果 机制
线程隔离(收益) 每线程读写各自 Thread.threadLocals,无共享、无锁
脏数据(风险) map 随线程而非任务存活 → 线程池复用线程时残留不清,下个任务读到上个任务的值
内存泄漏(风险) Entry 的 key 弱引用 ThreadLocal、value 强引用;ThreadLocal 被回收后 key=null,value 仍被池线程经 Entry 强引用 → 陈旧 Entry 无法释放

两个风险的根都是"map 绑在长生命周期的线程上"。对策同源:用完即 remove()——线程池场景必须在 finally 中清理(set/get 仅机会性清理部分 null-key 槽,不可依赖)。

演进:传递边界依次扩张

ThreadLocal 的副本严格绑定单线程,跨线程即断。两次演进都在扩张"传递边界",各自付出代价:

工具 扩张的边界 机制 局限/代价
ThreadLocal 单线程内 map 挂当前线程 跨线程断裂
InheritableThreadLocal 父→子线程 子线程构造时 childValue() 从父拷贝 池线程预创建复用,捕获不到"提交时"上下文,失效
TransmittableThreadLocal 跨线程池 提交时捕获快照,执行前 replay、执行后 restore 需装饰 Runnable/Callable 或线程池

TTL 的要害是时机分离:捕获在任务提交时(父线程上下文尚在),应用在任务执行前(已是池线程)。InheritableThreadLocal 把捕获绑死在线程创建时,而池线程创建早于任务提交,故必然漏掉提交时刻的上下文——这才是它在线程池失效的精确原因,而非笼统的"无法适配"。

适用边界:隐式传递是双刃

隐式传递省去层层透参,代价是数据流不可见:依赖被隐藏(损可测试性)、来源不显(损可读性)、跨层耦合。故 ThreadLocal 是"必要的恶"——框架内部用(事务管理、MDC 日志、SecurityContext),业务代码慎用,能显式传参则优先显式。

横向同构:ThreadLocal 是"每线程的隐式环境变量",与 Go context.Context、Kotlin CoroutineContext 同解一题,分歧在隐式(绑线程)vs 显式(随调用传递)。时间维度的趋势——虚拟线程下"每线程一副本"成本随线程数爆炸,JDK 以 ScopedValue(不可变、作用域绑定)替代之:隐式上下文正从线程绑定走向作用域绑定

选型方法:从问题域到工具

工具会朽,方法不朽。下面先给不随 JDK 变的决策路径,再给随之派生的工具表。

决策路径

  1. **定位问题域**:先判属六域中哪一类,并分清走的是"协调共享"主线,还是"不共享"支线(仅 ThreadLocal 系)
  2. **域内默认最简**:每个域有一个最简实现作默认(synchronized / Semaphore / RRWL / CountDownLatch / ThreadLocal)
  3. **仅在出现明确增量需求时升级**:没有增量需求就停在默认——"能用 synchronized 就不用 Lock"是这条规则的特例
  4. **横切域不单独选**:可见性、取消不挑工具,由所选工具自带(见表下注)

判据表

问题域 默认(最简) 出现以下增量需求时 升级 / 选用
竞争安全 · 临界区互斥(准入 N=1) synchronized 试锁 / 限时 / 可中断、多条件队列、公平、跨方法持锁 ReentrantLock
竞争安全 · 单变量原子 AtomicXxx(CAS) 多变量需一致 退回锁
竞争安全 · 读多写少 ReentrantReadWriteLock 读极短且状态可拷贝 → 乐观读;需重入 / Condition → 留 RRWL;写占比高 → 退回互斥 StampedLock
准入控制 N>1(配额 / 限流) Semaphore 进程外限流 令牌桶(见流量控制)
协作同步 CountDownLatch(等事件完成) 需复用 / 对等互等 → 栅栏;动态参与方 / 多阶段 → Phaser;成对换数据 → Exchanger CyclicBarrier / Phaser / Exchanger
上下文隔离与传播(不共享支线) ThreadLocal 父→子线程 → Inheritable;跨线程池 → 快照回放 InheritableThreadLocal / TTL

判据是"等待者是否参与计数""读临界区是否够短可乐观""上下文要跨多远"——是可执行的条件,不是"安全""高效"这类形容词。

表下注(横切两域,不单独选工具):

关联内容(自动生成)