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

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

分层自上而下是抽象层次递减,但稳定性不随之单调变化,而呈 U 形分布

稳定性 原因
问题空间 极稳定 并发的本体与基底问题,跨语言不变
抽象模型 稳定 准入 / 协作 / 上下文是跨平台通用抽象
构建路径 最易变 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 的本质

核心风险

InheritableThreadLocal 的局限

TransmittableThreadLocal(TTL)

工具选型的稳定方法论

并发问题 首选工具 原因
简单互斥 synchronized 简单、安全
高并发互斥 ReentrantLock 可控策略
读多写少 StampedLock 乐观并发
任务协作 CountDownLatch 一次性同步
阶段并行 CyclicBarrier 可复用
资源保护 Semaphore 配额控制
上下文传递 TTL 线程池友好

关联内容(自动生成)