在 Java 并发世界中,当并发冲突的概率变高、涉及多个变量的复合操作时,我们就需要从无锁方案跨入有锁的硬核控制区。本篇将深入底层源码与架构设计,带你透彻拆解从操作系统级的悲观锁 synchronized,到 JUC 框架的绝对基石 AQS,再到应对各种复杂工程场景的高阶锁工具库。

synchronized 是 Java 老牌的关键字,秉持 “悲观态度” (先加锁,再操作)。JVM 为了对它进行救赎,在其底层设计与运行期进行了一系列极其精妙的改动。
Java 中每个对象在堆内存中都有一个 对象头(Object Header) ,其核心区域称为 MarkWord。
在 JDK 1.6 之前,synchronized 只有“重量级锁”一种形态:
字节码层面:编译后对应 monitorenter(加锁进入临界区)和 monitorexit(解锁离开临界区)两条指令。
JVM 层面(核心) :每个锁对象都关联着一个 Monitor(监视器) ,内部包含三个核心逻辑区:
Owner:当前成功抢到锁、持有锁的线程。EntryList:抢锁失败,处于被动等待、陷入阻塞状态的线程队列。WaitSet:调用了 obj.wait() 后,主动让出锁并进入无限等待状态的线程队列,需等待 obj.notify() 唤醒。系统层面(性能瓶颈) :Monitor 底层强依赖操作系统的互斥量(Mutex Lock)。动用它意味着线程必须经历从用户态到内核态的上下文切换,开销极大,因而性能极差。
为了避免动辄呼叫操作系统的极高开销,JDK 1.6 引入了锁升级机制(状态只能升级,通常不可逆):
| 锁状态 | 适用竞争场景 | 底层动作与原理 | 设计思想 |
|---|---|---|---|
| 无锁 | 对象刚创建 | MarkWord 处于初始状态。 | 尚无竞争,无需保护。 |
| 偏向锁 | 无竞争 单线程反复重入 | 首次进入时,JVM 在 MarkWord 贴上当前线程ID。后续该线程再来,比对 ID 一致直接放行。 | 假设永远没有竞争,彻底省去加锁/解锁的 CAS 开销。 |
| 轻量级锁 | 轻微竞争 少量线程交替执行 | 出现竞争,偏向锁撤销。通过 CAS 尝试将 MarkWord 复制到线程栈并指向自己。失败的线程原地自旋(空跑 CPU)尝试抢锁。 | 假设很快就能拿到锁,宁愿耗费 CPU 自旋,也不去 OS 排队阻塞(避免用户态/内核态切换)。 |
| 重量级锁 | 激烈竞争 多线程高并发抢锁 | CAS 自旋次数过多,发生锁膨胀。直接动用 Monitor 和操作系统的 Mutex,抢锁失败的线程直接陷入阻塞状态,让出 CPU。 | 既然实在抢不到,自旋只会白白浪费 CPU,不如直接让线程休眠阻塞。 |
StringBuffer.append)。this(对象级别锁),互不相同的实例之间不冲突。Class 对象(类级别锁),该类的所有实例对象共用一把锁,全部互斥。CAS 只是微观的抢占状态动作。当大量线程抢锁失败、需要宏观的排队与精准唤醒时,JUC 的核心总调度官 AQS 闪亮登场。它是 ReentrantLock、Semaphore、CountDownLatch 的共同底层底座。
AQS 内部通过两个极其精妙的组件配合运转:
volatile int state (同步状态)
使用 volatile 保证多线程内存可见性。线程通过 CAS 操作去原子性地修改 state,修改成功即代表拿到了锁/资源。
0 为空闲,1 为被占用,>1 表示同一个线程的重入次数。CLH FIFO 双向队列 (等待队列)
抢锁失败的线程会被封装成一个 Node 节点,通过 CAS + 自旋安全地插入到双向链表的队尾。
waitStatus:当其变为 SIGNAL (-1) 时,表示当前节点对应的线程已经挂起(通过 LockSupport.park() 实现),且它的前驱节点在释放锁时,有义务将其唤醒。AQS 框架把复杂的线程排队、阻塞挂起、出队、高并发下节点安全插入等一整套宏观流程写死在了父类的方法中(如 acquire()、release())。它把如何定义资源、如何尝试抢占资源的微观逻辑(tryAcquire()、tryRelease())以保护方法的形式留给子类去重写实现。
lock())compareAndSetState(0, 1) 发起 CAS 抢锁。抢到了就把 ExclusiveOwnerThread 设为自己。tryAcquire) :如果第一步没抢到,检查 state。若是当前锁的持有者就是自己,则触发可重入机制,执行 state + 1 并直接放行;若不是自己,抢锁宣告失败。addWaiter) :抢锁失败的线程被包装成 Node 节点,通过 CAS 自旋尾插法安全地送入 FIFO 双向链表的末尾排队。acquireQueued) :节点入队后,会检查自己的前驱是不是 Head 头节点。如果是,则做最后一次 tryAcquire 挣扎抢锁;如果仍抢不到,则将前驱的状态强行改为 SIGNAL (-1),随后调用 LockSupport.park(this) 强行挂起休眠,让出 CPU。unlock())tryRelease) :当前持有锁的线程调用 unlock(),内部将 state 减 1。state 减到 0 的时候,才会彻底清空锁的占有者 ExclusiveOwnerThread = null,宣告锁彻底空闲。unparkSuccessor) :锁释放后,头节点 Head 负责牵头,找到队列中第一个有效等待的 Node 节点,调用 LockSupport.unpark(node.thread) 精准将其唤醒,重新起来抢锁。hasQueuedPredecessors()NonfairSync) :极其霸道。线程一上来直接 CAS 暴力抢锁,抢不到再去排队。优点是能充分利用线程唤醒的时间差让新来的线程直接把活干了,吞吐量极大。FairSync) :严格讲究先来后到。它的 tryAcquire 源码里多了一行标志性的核心判断:hasQueuedPredecessors() 。该方法会检查: “当前排队的队列里,我前面是不是还有人在排队?” 如果有人在排队,公平锁会强行放弃抢占,乖乖去队尾排队。其缺点是会引发高频的线程上下文切换,性能大幅下滑。Java 并发包在不同场景演进下,衍生出了四种经典的锁控制方案:
| 对比维度 | synchronized | ReentrantLock | ReentrantReadWriteLock | StampedLock |
|---|---|---|---|---|
| 锁的本质 | 独占锁 / 悲观锁 | 独占锁 / 悲观锁 | 读写分离(读共享,写独占) | 读写分离 + 乐观读机制 |
| 实现层面 | JVM 关键字(基于 Monitor) | JDK API 层(基于 AQS) | JDK API 层(基于 AQS) | JDK API 层(非 AQS 架构) |
| 释放方式 | 隐式自动释放 | 必须在 finally 中手动 unlock() | 必须手动显式释放 | 必须手动释放(凭邮戳 Stamp 释放) |
| 公平性 | 仅支持非公平锁 | 支持公平与非公平 | 支持公平与非公平 | 仅支持非公平锁 |
| 功能扩展 | 简单,具备锁升级优化 | 支持可中断、可超时、支持多条件变量 Condition | 针对“读多写少”高频读取场景优化 | 引入乐观读,极限压榨读取性能 |
ReentrantReadWriteLock 的致命痛点:写饥饿
虽然读写锁实现了“读读共享、读写互斥、写写互斥”,极大提升了读取吞吐量。但是,如果线上有源源不断、铺天盖地的读请求疯狂涌入,读锁就一直被占用且无法释放,导致后台的写请求线程只能被迫无限期阻塞罚站,最终被 “饿死” 。
StampedLock 的极限压榨:乐观读 (Optimistic Read)
为了彻底干掉“写饥饿”,StampedLock 横空出世。它在读数据的时候,根本不加任何真正的锁!而是直接返回一个版本号邮戳(Stamp)。
线程全程无阻碍地盲读数据,读完之后,通过调用 validate(stamp) 校验一下在刚才盲读的期间,有没有写线程动过数据。如果没人动过,全程无锁执行,性能无敌;如果发现数据被写动了,它才会认命,降级为传统的悲观读锁重新读取,完美消除了写饥饿。
synchronized!代码最清爽,JVM 自带锁升级,绝无漏掉释放锁而引发死锁的风险。tryLock)、响应中断、或者需要利用多条件变量 Condition 实现类似“奇偶数精准交替唤醒”的高阶逻辑时,换成 ReentrantLock。ReentrantReadWriteLock。StampedLock。除了互斥抢锁外,JUC 基于 AQS 的 共享模式 封装了三个应对复杂业务流的顶级协作 API。
核心作用:让主线程(或某个等待线程)陷入阻塞,死等 N 个子线程并发执行完毕后才能继续往下走。常用于并行加载多源数据(如拼装电商详情页:并发查商品、查库存、查营销,最后合并返回)。
工业级标准防漏模板
使用 CountDownLatch 时,务必将 countDown() 扣减计数的操作放在子线程的 finally 代码块中!防止因为业务逻辑突发异常抛出导致 countDown() 错失执行,让等待的主线程陷入永久死锁的灾难。
Java
// 初始化计数器为 3
CountDownLatch latch = new CountDownLatch(3); executor.submit(() -> {
try {
// 执行耗时远程 RPC 调用或营销风控计算...
} finally {
latch.countDown(); // 【铁律】确保无论成败,计数器必然扣减
}
});latch.await(); // 主线程在此阻塞,直到计数器归零
(注:CountDownLatch 是一次性的,计数器扣完归零后无法重置复用。)
acquire() 成功抢到许可证(state > 0)才能进去执行,干完活调用 release() 归还车位。如果把许可证总数初始化设为 1,它就能直接退化成一把互斥锁来使用。CyclicBarrier 内部的计数器在所有人跨过栅栏后会自动重置,支持在循环业务中反复复用。 线程试图进入
↓
尝试修改 state
↓
┌────────────────────┼────────────────────┐
▼ ▼ ▼
[ReentrantLock] [Semaphore] [CountDownLatch]
state == 0? state > 0? state == 0?
(独占模式:锁空闲) (共享模式:有资源) (共享模式:倒计时完)
│ │ │
├────────────────────┴────────────────────┘
├─────────────────── 成功 ───────────────────► 继续执行
▼
失败:AQS 接管宏观框架
↓
封装为 Node 节点,通过 CAS 安全尾插法送入 FIFO 双向链表
↓
标记前驱状态为 SIGNAL (-1)
↓
调用 Unsafe.park() 强行挂起当前线程,进入阻塞休眠
↓
【后续锁释放 / 资源归还 / 计数器归零】
↓
触发调用 LockSupport.unpark() 唤醒排在前面的有效 Node 节点
↓
线程睁眼,重新发起 CAS 抢占修改 state