JAVA AQS与Lock实现原理(含CAS)

07 · AQS 与 Lock 实现原理(含 CAS)

对应学习计划:第一月第2周「Java 并发编程(重中之重!薪资分水岭)」

面试定位:AQS 是 JUC 的「地基工程」,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 全部建立在它之上。懂 AQS = 看懂了 JUC 半边天。


一、本文能帮你回答的面试题

  1. 什么是 AQS?它的核心思想是什么?
  2. AQS 的数据结构(state + CLH 队列)?为什么用 CLH 变体队列?
  3. ReentrantLock 源码:lock/unlock 流程?
  4. 公平锁和非公平锁的区别?(为什么不公平锁性能反而好)
  5. ReentrantLock 和 synchronized 的区别?
  6. CAS 是什么?有什么缺陷?怎么解决 ABA?
  7. Semaphore / CountDownLatch / CyclicBarrier 各自什么场景?

二、概念介绍

AQS(AbstractQueuedSynchronizer,抽象队列同步器):JUC 锁的基础框架。核心思想:

用「一个状态变量 state + 一个 FIFO 等待队列」管理所有线程的获取/释放资源的同步逻辑。

java 复制代码
public abstract class AbstractQueuedSynchronizer {
    private volatile int state;                 // 同步状态
    // 内部类:CLH 变体双向队列(把抢不到锁的线程排成队)
    static final class Node {
        volatile Node prev, next;
        volatile Thread thread;                 // 队里等着谁
        volatile int waitStatus;                // 节点状态(-1=SIGNAL 要唤醒下一个)
    }
    private transient volatile Node head, tail; // 队首队尾
}

AQS 提供两种模式:

  • 独占模式(EXCLUSIVE):一把锁只有一个线程持有,如 ReentrantLock。
  • 共享模式(SHARED):多个线程可以同时持有,如 Semaphore、CountDownLatch。

继承体系(面试组织答案的骨架):

复制代码
Lock 接口
 └── ReentrantLock ── 内部 Sync extends AQS(FairSync/NonfairSync)
     ReentrantReadWriteLock.ReadLock / WriteLock ── AQS
     Semaphore(共享计数)─ AQS
     CountDownLatch ── AQS(共享)
     CyclicBarrier ── 基于 ReentrantLock + Condition(不是直接 AQS)

三、原理深入

3.1 CLH 队列:为什么无法抢到锁的线程要进队列阻塞

  • 直接自旋空转浪费 CPU;直接进 OS 挂起又太贵。
  • CLH 变体队列:刚到的线程先自旋几次(很短),不行就把自己包装成 Node 挂到队尾,然后 park(挂起)。
  • 前驱节点释放时,会通过 unpark 唤醒后继节点------避免惊群(只唤醒一个)。

3.2 CAS(Compare And Swap)------AQS 和 JUC 的原子基石

java 复制代码
// 语义:比较-交换,硬件级原子指令(CPU 保证)
// 当前值 == 期望值 ? 改成新值,返回 true : 返回 false,改不了
boolean compareAndSet(int expect, int update);
// 底层:Unsafe.compareAndSwapInt(object, offset, expect, update)

CAS 三个参数 :内存位置、期望值、新值。核心价值:不加锁实现并发安全(乐观锁思想)。

CAS 缺陷(必考):

  1. ABA 问题 :变量 A 被改成 B 又改回 A,CAS 发现值没变就成功了,但中间被改过。解决:AtomicStampedReference(带版本号)。
  2. 自旋开销:竞争激烈时 CPU 空转。
  3. 只能保证单个变量原子 :多个变量需要锁或包装成对象(AtomicReference)。

3.3 ReentrantLock 核心流程

复制代码
acquire(1)  (以非公平锁为例)
 ├─ CAS 尝试 state 0→1
 │   成功 → 当前线程持锁
 │   失败 ↓
 ├─ 判断是否可重入(当前线程 == exclusiveOwnerThread?)
 │   是 → state += 1(重入计数)
 │   否则 ↓
 └─ 加入 CLH 队尾(Tail CAS 入队)→ 自旋/park 等待
    前驱节点释放后把它唤醒 → 醒来后再次 tryAcquire

release(1)
 └─ state -= 1
    若 state == 0 → 真正释放独占 → 唤醒队头后继节点

非公平 vs 公平:

java 复制代码
// 非公平:线程直接抢一次(插队),抢不到才排队
final boolean nonfairTryAcquire(int acquires) {
    if (compareAndSetState(0, acquires)) { ... }   // 先插队
    ...
}
// 公平:先看队列有没有人,有就不抢
protected final boolean tryAcquire(int acquires) {
    if (hasQueuedPredecessors()) return false;     // 队列非空就让位
    ...
}

为什么非公平锁更快:新线程「插队成功」就不用做线程上下文切换(park + unpark 很贵);公平锁强制排队,队尾总是被唤醒-挂起-再唤醒,切换成本高。所以 JDK 的 ReentrantLock 默认非公平,吞吐更大(代价是「饿死」风险,但统计上可接受)。

3.4 Condition:精确唤醒(synchronized wait/notify 的替代)

java 复制代码
// Condition 绑定在 Lock 上,每个 Condition 一条独立等待队列
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 生产:lock.lock(); while (队列满) notFull.await(); 入队; notEmpty.signal(); unlock();
// 消费:lock.lock(); while (队列空) notEmpty.await(); 出队; notFull.signal(); unlock();

对比 old synchronized:只能 wait/notifyAll 全部唤醒;Condition 可以定向唤醒(唤醒生产者 或 消费者),减少无谓唤醒。

3.5 ReentrantLock vs synchronized(终极对比表)

维度 synchronized ReentrantLock
使用 关键字,无感 API,必须手动 unlock(finally)
锁粒度 方法/代码块 更灵活(tryLock、lockInterruptibly)
可否限时获取 ❌ ✅ tryLock(1, TimeUnit.SECONDS)
可中断 ❌ ✅ lockInterruptibly()
公平策略 非公平 可指定公平/非公平
条件队列 1 个 多个 Condition 精确唤醒
性能 JDK6 后大幅优化,两者相当 相当
底层 Monitor(ObjectMonitor) AQS(CLH + LockSupport)

给面试的结论:JDK6 优化后 synchronized 性能已追平 ReentrantLock;优先用 synchronized(简单、不易错),只有需要「限时获取、可中断、多条件、公平」时才用 ReentrantLock。


四、实战例子

实战 1:经典并发场景对比------用 ReentrantLock 实现限时抢锁

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockDemo {
    private static final ReentrantLock LOCK = new ReentrantLock();

    public static void main(String[] args) {
        // 线程1 长期持有
        new Thread(() -> {
            LOCK.lock();
            try { Thread.sleep(5000); } catch (Exception e) {}
            finally { LOCK.unlock(); }
        }, "占用者").start();

        // 线程2:最多等2秒,抢不到就放弃(synchronized 做不到!)
        Runnable task = () -> {
            boolean ok = false;
            try { ok = LOCK.tryLock(2, TimeUnit.SECONDS); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            System.out.println(Thread.currentThread().getName() + " 抢锁结果: " + ok);
            if (ok) LOCK.unlock();
        };
        new Thread(task, "面试者A").start();
        new Thread(task, "面试者B").start();
    }
}

实战 2:手写一个「先进先出队列」供生产者-消费者使用(Condition 实战)

java 复制代码
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BlockingQueueDemo<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 队列不满
    private final Condition notEmpty = lock.newCondition();  // 队列不空

    public BlockingQueueDemo(int capacity) { this.capacity = capacity; }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) notFull.await();  // 满了等
            queue.add(item);
            notEmpty.signal();                                 // 精准唤醒消费者
        } finally { lock.unlock(); }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) notEmpty.await();          // 空了等
            T item = queue.poll();
            notFull.signal();                                  // 精准唤醒生产者
            return item;
        } finally { lock.unlock(); }
    }
}

实战 3:ABA 问题的演示与解决

java 复制代码
import java.util.concurrent.atomic.*;

public class AbaDemo {
    public static void main(String[] args) throws InterruptedException {
        // 有 ABA:值 A -> B -> A,CAS 误判「没被改过」
        AtomicInteger num = new AtomicInteger(10);
        // 用带版本的解决:AtomicStampedReference
        AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10, 0);

        int stamp = ref.getStamp();
        // 模拟:线程A做 CAS(10 -> 20),期间有人把值改了又改回来
        ref.compareAndSet(10, 12, stamp, stamp + 1);   // 版本号也变了,能感知
        System.out.println("当前值: " + ref.getReference() + ", 版本: " + ref.getStamp());
    }
}

实战 4:Semaphore(信号量)------控制并发访问数

java 复制代码
import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    // 只允许 3 个线程同时访问(如数据库连接池只放行3个)
    private static final Semaphore SEM = new Semaphore(3);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    SEM.acquire();
                    System.out.println(Thread.currentThread().getName() + " 进入,剩余许可: " + SEM.availablePermits());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    SEM.release();
                }
            }, "线程" + i).start();
        }
    }
}

五、面试话术(背下来)

Q:AQS 是什么?

「AQS 是 JUC 锁的统一框架,核心是一个 volatile 的 int state + CLH 变体双向等待队列。获取资源成功就改 state;失败就把线程包装成 Node 挂到队列里 park 挂起;释放时唤醒后继。独占/共享两种模式,ReentrantLock/Semaphore/CountDownLatch 都是基于它,只需要子类实现 tryAcquire/tryRelease 决定什么时候算获取成功。」

Q:ReentrantLock 的原理?

「内部 Sync 继承 AQS。lock()=acquire(1):先尝试 CAS state 0→1,成功持锁;失败则看是不是当前线程重入(state+1);不是就 CAS 入队等待。unlock()=release(1):state-1,减到 0 真正释放并唤醒队头。支持重入靠 exclusiveOwnerThread 记录持锁线程。」

Q:公平锁和非公平锁?

「非公平锁是线程进来直接先 CAS 试抢一次,抢不到才排队------优点是减少了大量 park/unpark 上下文切换,吞吐高,所以默认非公平;缺点是可能造成队尾线程饿死。公平锁先查 hasQueuedPredecessors,队列有人就让位。」

Q:CAS 缺点?

「三个:ABA 问题(用 AtomicStampedReference 加版本号解决);自旋消耗 CPU(竞争激烈时不划算);只能原子操作单个变量(多个字段一致性要用锁或 AtomicReference 包结构体)。」

Q:什么时候用 ReentrantLock?

「需要 tryLock 限时抢锁、lockInterruptibly 可中断、多个 Condition 精确唤醒、公平锁时用它;简单互斥场景 synchronized 更简洁,JDK6 后性能也差不多。」


六、踩坑总结(易错点)

# 坑 说明
1 忘记在 finally 里 unlock 抛异常后锁永不释放,直接死锁
2 lock 顺序不一致导致死锁 多把锁要按固定顺序获取
3 lock() 后还 tryLock 语义混淆;tryLock 失败不算「获得锁」,不能在 finally 无脑 unlock
4 Condition 的 await 忘 while 和 wait 一样要 while 防虚假唤醒
5 把 Semaphore 当「有界队列」 它只限并发量,资源释放要靠 acquire/release 配对,漏 release 会泄漏许可
6 CountDownLatch 不可复用 一次性;要复用用 CyclicBarrier
7 没考虑中断 lockInterruptibly/tryLock 抛 InterruptedException 必须恢复中断位或处理

七、课后作业

  1. 手写 ReentrantLock 的核心(state + 重入 + CLH 简易队列),验证互斥。
  2. 用 CountDownLatch 并行查询「4 个上游接口」,全部返回后聚合结果(配合 CompletableFuture 更优雅,见第10篇)。
  3. 思考:为什么 AQS 用双向队列而不是单向队列(提示:需要支持中途取消节点/查找前驱状态)。
  4. 对比 CyclicBarrier 与 CountDownLatch,各举一个业务场景。

下一篇预告:线程池七大参数与四种拒绝策略。

相关推荐
爱喝水的鱼丶2 小时前
SAP-ABAP:ECC→S/4HANA ABAP 迁移升级指南——程序改造、语法兼容与性能优化 导语
运维·开发语言·性能优化·sap·abap·经验交流·开发交流
w_zero_one2 小时前
算法--模拟
java·数据结构·算法
小灰灰搞电子2 小时前
Rust+Slint 实现FTP客户端调试助手源码分享
开发语言·后端·rust
外收内放2 小时前
面向对象高级(实战案例)
开发语言·python·学习
Javatutouhouduan2 小时前
阿里1658页Java面试题总结(含答案)
java·高并发·java面试·java面试题·后端开发·java程序员·java八股文
秋天的怀念_David2 小时前
算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫
java·程序人生·算法
专业程序开发源3 小时前
springboot欧缇蔓月子中心管理系统82697-计算机课程设计、毕业设计
java·spring boot·后端·python·spring·django·课程设计
秋天的怀念_David3 小时前
算法一千零一夜 · 第十四夜 幻方与奇妙的二进制
java·程序人生·算法
thefool1122663 小时前
网络编程^
java