--- tags: [java/lang, lock-escalation, CAS, AQS, optimistic-lock] create time: 2026-08-08 18:00 update time: 2026-08-08 18:00 --- # 锁升级与 CAS 机制 ## 概述 Java 的 synchronized 关键字从 JDK 1.5 到 JDK 1.6 经历了一次重大升级——引入锁升级机制,让锁在低竞争时以极轻量方式运行,在高竞争时平滑过渡到重量级互斥锁。这一设计的核心是 **CAS(Compare And Swap)** 原子操作和 **AQS(AbstractQueuedSynchronizer)** 框架,它们共同构成了 Java 并发包的底层基石。 ## 核心原理 ### 锁升级之路:偏向 → 轻量级 → 重量级 synchronized 的锁状态不是静态的,它会随着竞争程度逐步"膨胀": ```mermaid stateDiagram-v2 [*] --> 无锁状态 无锁状态 --> 偏向锁: 第一个线程获取锁 偏向锁 --> 偏向锁: 同一线程重复获取\n(时间戳比较) 偏向锁 --> 轻量级锁: 其他线程尝试获取\n(发生竞争) 轻量级锁 --> 轻量级锁: CAS自旋成功\n(少数竞争者) 轻量级锁 --> 重量级锁: CAS失败次数过多\n或自旋超时 重量级锁 --> 轻量级锁: 竞争减弱\n(Monitor退出队列) ``` #### 第一阶段:偏向锁(Biased Locking) **触发条件**:第一个线程进入同步块时,JVM 会在对象头中记录当前线程 ID,后续该线程再次进入时无需任何同步操作。 - JDK 6 默认启用,需要 `-XX:+UseBiasedLocking`。 - JDK 15 中被标记为废弃,JDK 17 中被移除。因为实际场景中"永远只有一个人访问"的概率很低,而偏向锁的撤销代价较高(需全局 STW 撤销所有偏向)。 **对象头结构(HotSpot,64位)**: | 偏移 | 字段 | 大小 | |------|------|------| | 0-2 bit | Mark Word 低 3 位 | 锁标志位 | | 3-31 bit | 偏向锁标记 + ThreadID | 优先权 + 线程 ID | | 32-63 bit | 分代年龄 + hashCode | 64 位扩展信息 | 当有其他线程尝试获取偏向锁时,JVM 会撤销该对象的偏向状态,升级为轻量级锁。 #### 第二阶段:轻量级锁(Lightweight Locking) **触发条件**:偏向锁被剥夺后,或者从一开始就存在多个线程竞争。 核心机制:**利用 CAS 替换对象头中的 Mark Word**。 流程: 1. 线程在栈帧中创建 **Lock Record**,复制对象头的 Mark Word 到其中。 2. 使用 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针。 3. 如果 CAS 成功,当前线程获得锁;如果失败,说明有竞争。 4. 竞争发生时,当前线程自旋重试(最多循环指定次数)。 5. 如果自旋超过阈值(默认 10 次,可通过 `-XX:PreBlockSpin` 调整),升级为重量级锁。 > [!NOTE] > "轻量级"并不意味着不需要操作系统内核帮助——只是在没有竞争时使用用户态 CAS,避免了上下文切换开销。一旦竞争激烈,它最终会退化为重量级锁。 #### 第三阶段:重量级锁(Heavyweight Locking) **触发条件**:轻量级锁的 CAS 和自旋都未能成功获取锁。 此时 Monitor 对象成为真正的互斥锁: - 未获得锁的线程会被阻塞挂起(进入 OS 的等待队列)。 - 阻塞/唤醒操作需要切换到内核态,这是性能损耗最大的环节。 ### Unsafe 类与 CAS 原理 `Unsafe` 是 JVM 提供的一个"后门"类,允许 Java 代码直接操作内存和执行原子操作。其中的 CAS 原语是所有乐观锁的基础。 **核心方法签名**: ```java public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x); ``` `offset` 是通过 `objectFieldOffset(Field)` 计算出的字段在对象内存布局中的字节偏移量。 **原理**:CAS 是 CPU 级别的原子指令(x86 下对应 `cmpxchg` 汇编),在多核环境下通过总线锁或缓存锁保证原子性。JVM 内部通过 `Atomic*` 类封装了这些操作,开发者无需直接使用 Unsafe。 ```java // AtomicInteger.incrementAndGet 的核心逻辑(简化版) private volatile int value; public final int incrementAndGet() { int prev, next; do { prev = get(); // 读取当前值 next = prev + 1; // 计算新值 } while (!compareAndSet(prev, next)); // CAS 更新 return next; } ``` 这段代码用 12 行实现了线程安全的自增——没有使用任何 synchronized。其关键保障来自 `while` 循环:如果 CAS 失败(说明中间有其他线程修改了 value),就重新读取、重新计算、重新尝试,直到成功。 > [!WARNING] > CAS 的三个问题: > 1. ABA 问题(见下文) > 2. 只能保证一个共享变量的原子操作,无法做多变量联合更新 > 3. 长时间自旋会增加 CPU 开销——这就是为什么有界锁最终会升级为重量级锁 #### ABA 问题与 AtomicStampedReference ABA 问题是 CAS 的经典缺陷:线程 T1 读取值为 A,另一个线程 T2 把值改为 B 再改回 A,T1 的 CAS 检查时发现值仍是 A,误以为没有被修改过。 解决思路:**给值加版本号**——每次变更版本号加 1。即使值回到 A,版本号也已不同。 ```java // 伪代码示意 AtomicStampedReference 用法 AtomicStampedReference ref = new AtomicStampedReference<>("A", 0); int[] stampHolder = new int[1]; String current = ref.get(stampHolder); // 返回 ["A", 0] int stamp = stampHolder[0]; // CAS 时需要同时匹配值和版本号 ref.compareAndSet("A", "B", stamp, stamp + 1); // [A, 0] -> [B, 1] ref.compareAndSet("B", "A", stamp + 1, stamp + 2); // [B, 1] -> [A, 2] ``` ### AQS(AbstractQueuedSynchronizer)核心设计 AQS 是 `java.util.concurrent` 包的基石——ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore 全部基于它构建。 #### CLH 队列模型 AQS 维护了一个 FIFO 的等待队列(CLH 变体),每个节点代表一个等待资源的线程: ```mermaid flowchart LR H["head"] --> N1["Node 1
WAITING"] N1 --> N2["Node 2
SIGNAL"] N2 --> N3["Node 3CONDITION"] N3 --> NULL["null (tail)"] style H stroke-dasharray: 5 5 ``` 核心规则: - **头节点(head)** 是当前持有锁的节点,它的线程正在执行。 - **尾节点(tail)** 是新入队节点的插入位置。 - 前驱节点释放锁时会唤醒后继节点(通过 `park()` → `unpark()`)。 - 节点状态包括:`CANCELLED`(已取消)、`SIGNAL`(后继需 unpark)、`CONDITION`(等待条件变量)、`PROPAGATE`(共享模式传播)。 #### state 状态机 AQS 的核心是一个 `volatile int state` 变量,表示同步状态: | 同步器 | state 含义 | |--------|-----------| | ReentrantLock | 持有锁的次数(重入计数) | | CountDownLatch | 计数器初始值,递减到 0 触发 | | Semaphore | 可用许可证数量 | | ReentrantReadWriteLock | 高 16 位读计数,低 16 位写计数 | #### tryAcquire / tryRelease 模板方法 子类只需实现这两个抽象方法,AQS 处理所有队列管理细节: ```java // ReentrantLock 的非公平锁 tryAcquire 核心逻辑(简化) protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 无竞争时直接用 CAS 拿锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 重入:累加 state setState(c + acquires); return true; } return false; // 竞争发生,走 AQS 排队流程 } ``` > [!TIP] > 面试常考点:AQS 的 `setState()` 用了 `volatile` 但非 CAS——因为重入场景下只增加不减少,且由独占线程自己操作,不存在多线程竞写问题。 ### 三大常用工具类的 AQS 应用 #### CountDownLatch 单向计数器,减到 0 后释放所有等待线程。**不可重置**,适合"等齐事件"场景。 ```java // 主线程等待 5 个初始化任务完成 CountDownLatch latch = new CountDownLatch(5); for (int i = 0; i < 5; i++) { executor.submit(() -> { doInit(); latch.countDown(); // 每完成一个减 1 }); } latch.await(); // 阻塞直到计数器归零 ``` #### CyclicBarrier 可循环使用的栅栏,到达指定数量后统一放行。**可以复用**,适合多阶段并行计算。 ```java // 三组数据并行处理,完成后合并结果 CyclicBarrier barrier = new CyclicBarrier(3, resultMerger::merge); for (DataSource ds : dataSources) { executor.submit(() -> { Result r = ds.process(); barrier.await(); // 等待同伴 }); } ``` #### Semaphore 控制并发访问的资源信号量。常用于限流——限制同时运行的任务数。 ```java // 限制同时执行 10 个 IO 请求 Semaphore sem = new Semaphore(10); executor.submit(() -> { sem.acquire(); // 拿令牌,不足则阻塞 try { ioRequest.send(); } finally { sem.release(); // 归还令牌 } }); ``` ## 实践场景 **面试高频对比题**: | 维度 | synchronized | ReentrantLock | |------|-------------|---------------| | 实现层级 | JVM 内置关键字 | JDK API 层 | | 锁升级 | 偏向→轻量级→重量级 | 无(始终 AQS + CAS) | | 公平性 | 非公平 | 可选公平/非公平 | | 中断响应 | 不响应中断 | `lockInterruptibly()` 支持 | | 条件变量 | wait()/notify() | 多个 Condition | | 超时获取 | 不支持 | `tryLock(timeout)` | | 可重入 | 是 | 是 | ## 关联笔记 - [[01.Java/concurrent/线程池参数与拒绝策略]]