【031】面试被问 AQS 直接懵?王二连说 3 个 “不知道”,哇哥用门禁系统讲透核心!


请添加图片描述

零、引入

“面试官最后问‘ReentrantLock 底层靠啥实现的?’我答‘锁啊’,再问‘AQS 懂吗?’我直接卡壳,全程‘不知道’三连,出门就收到‘不合适’的短信……” 王二攥着面试反馈,蹲在工位角落,刚面的大厂 offer 就这么飞了,连面试官摇头的样子都记得清清楚楚。

隔壁哇哥嚼着口香糖凑过来,一把抢过反馈单:“你光会用 ReentrantLock 不行啊,*AQS 才是并发工具的‘骨架’——ReentrantLock、CountDownLatch 这些全是靠它撑起来的. *今天起分三篇给你讲,第一篇先把 AQS 的‘魂’给你扒出来,用公司门禁系统给你讲明白,下次面试让你把面试官说懵。”

点赞 + 关注,跟着哇哥和王二,从 “门禁管理员” 到手写代码,彻底搞懂 AQS 的核心,并发面试再不怕!

一、先破案:王二的 “知识盲区”,AQS 到底是个啥?

请添加图片描述
王二打开 IDE,指着 ReentrantLock 的代码:“我知道 lock () 能加锁,unlock () 能解锁,可面试官一追问‘底层怎么实现的’,我就懵了。”

哇哥敲了敲键盘,调出 AQS 的类图:“AQS 全名叫 AbstractQueuedSynchronizer,翻译过来是‘抽象队列同步器’—— 听着玄乎,其实就是个‘并发工具的模板’。你可以把它想成公司的门禁管理系统:

  • 门禁系统(AQS):规定了谁能进公司(获取锁)、谁要排队(等待队列)、进不去怎么办(阻塞);
  • 员工卡(同步状态):一张卡代表一个‘锁’,卡被人拿走了(状态 = 1),其他人就得排队;
  • 排队区(CLH 队列):没拿到卡的人,按顺序排好队,等前面的人还卡(释放锁)再轮;
  • 具体部门(ReentrantLock/CountDownLatch):不同部门用门禁系统的规则不一样 —— 研发部要‘独占卡’(一个人用),人事部要‘共享卡’(多个人一起用)。”

王二眼睛一亮:“也就是说,AQS 是个‘通用门禁框架’,ReentrantLock 这些工具只是在这个框架上套了层‘部门规则’?”

“太对了!” 哇哥点头,“AQS 把并发工具的共性(排队、阻塞、唤醒)都写好了,剩下的‘部门规则’(比如独占还是共享、怎么判断能不能拿卡)留给具体工具自己实现 —— 这就是模板方法模式,AQS 是模板,ReentrantLock 是具体实现。”

二、AQS 的核心三要素:同步状态 + CLH 队列 + 条件队列

哇哥拉过一张纸,画了个简易的 AQS 结构图,王二一看就懂了 —— 核心就三样东西,连起来就是完整的 “门禁流程”。

✨ 同步状态:AQS 的 “核心开关”(员工卡)

AQS 里有个state变量,是 volatile 修饰的,专门记录 “锁的状态”,这是判断能不能拿锁的关键:
在这里插入图片描述

  • 对于 ReentrantLock(独占锁):state=0代表锁没人用,state=1代表锁被占用,state>1代表重入(同一个人拿了多次卡);
  • 对于 CountDownLatch(共享锁):state=N代表还有 N 个任务没完成,state=0代表所有任务完成,等待的线程可以进。

“这就像门禁系统的‘卡状态显示器’,” 哇哥比喻,“显示‘空’(state=0)就能拿卡,显示‘已占用’(state=1)就得排队。”

AQS 给state提供了三个核心操作,都是原子的,靠 CAS 实现(保证多线程安全):

getState():看当前状态(查显示器);
setState(int newState):改状态(手动掰显示器,只有拿到卡的人能改);
compareAndSetState(int expect, int update):CAS 改状态(比如期望是 0,改成 1,改成功说明拿到卡了)。

📢 2. CLH 队列:AQS 的 “排队区”(按顺序等卡)

CLH 队列是个双向链表,专门存 “没拿到锁的线程”—— 就像公司门口的排队队伍,按先来后到顺序排,前面的人还了卡,就叫醒下一个人。

队列里的每个节点(Node)都有这些关键信息:
在这里插入图片描述

thread:排队的线程(哪个员工在等);
prev/next:前后节点的指针(排队的人互相拉着手,不插队);
waitStatus:节点状态(比如 “等待中”“已取消”“要唤醒”)。

“重点是‘双向链表’,” 哇哥强调,“既能往前找前面的人,也能往后找后面的人,方便移除取消排队的线程,也方便唤醒下一个人。”

➡️ 条件队列:AQS 的 “特殊候场区”(等通知再排队)

在这里插入图片描述

除了 CLH 主队列,AQS 还有 “条件队列”—— 比如 ReentrantLock 的 Condition,就靠这个实现。

“这就像公司的‘会议室候场区’,” 哇哥比喻,“你拿到门禁卡进了公司(获取锁),但会议室有人(条件不满足),就去候场区等,等里面的人出来喊你(signal),你再回到主队列重新排队拿会议室的钥匙。”

条件队列也是个链表,线程调用await()就进条件队列,调用signal()就从条件队列移回 CLH 主队列,等待拿锁。

三、AQS 的核心工作流程:拿锁→排队→唤醒(门禁系统实战)

请添加图片描述
哇哥用 “研发部员工小王拿门禁卡” 的场景,给王二讲透 AQS 的完整流程 —— 这也是 ReentrantLock 的底层逻辑:

✔️ 流程 1:拿锁(尝试拿员工卡)

小王(线程)到门禁口,先查状态显示器(getState ()):

  • 如果显示 “空”(state=0),用 CAS 把状态改成 1(compareAndSetState (0,1)),改成功就拿到卡,进公司(获取锁成功);
  • 如果显示 “已占用”(state=1),再看是不是自己拿的(ReentrantLock 支持重入)—— 如果是,就把 state 加 1(比如 state=2),不用排队;
  • 如果是别人拿的,就去 CLH 队列排队。

📌 流程 2:排队(进 CLH 队列等)

小王没拿到卡,AQS 会做三件事:

  1. 把小王包装成一个 Node 节点,加到 CLH 队列的末尾(CAS 加,保证线程安全);
  2. 让小王的线程阻塞(用 LockSupport.park (),比 wait () 更灵活);
  3. 释放 CPU 资源,小王的线程进入 “阻塞状态”,不再抢锁。

“这一步很关键,” 哇哥说,“线程阻塞后就不占 CPU 了,不会像王二之前写的空转代码那样把 CPU 跑满。”

✅ 流程 3:唤醒(前面的人还卡,叫醒下一个)

拿卡的人(线程)离开公司,执行 unlock ():

  1. 把 state 改成 0(释放卡);
  2. 从 CLH 队列的头节点开始,找下一个 “等待中” 的节点(小王);
  3. 用 LockSupport.unpark () 唤醒小王的线程;
  4. 小王被唤醒后,再次尝试 CAS 改 state 拿锁 —— 这次大概率能拿到,因为前面的人已经还卡了。

“整个流程就是‘拿锁→排队→唤醒→再拿锁’,”

王二总结,“AQS 把这些流程都写死了,ReentrantLock 只需要告诉 AQS‘怎么判断能不能拿锁’‘拿锁后怎么改状态’就行?”

“完全正确!” 哇哥竖大拇指,“这就是模板方法模式的精髓 ——AQS 定义骨架,具体工具填充细节。”

四、条件队列:门禁的 “VIP 候场区”

如果公司有 VIP 通道,普通排队区之外还要有个 VIP 候场区 —— 这就是 AQS 的 “条件队列”,配合 Condition 使用,对应await()和signal()方法。

➡️ 条件队列的核心逻辑(VIP 候场场景)

  1. VIP 用户王二进门后,发现会议室被占了(业务条件不满足),就去 VIP 候场区等着(调用 await (),从 CLH 队列转到条件队列);
  2. 会议室空了(业务条件满足),前台小姐姐喊一声(调用 signal ()),王二从 VIP 候场区回到 CLH 队列的队头,优先进门;
  3. 条件队列可以有多个,比如 “会议室候场区”“打印机房候场区”,对应不同的业务条件。

五、手写简易 AQS:自己实现一个 “独占锁”

为了让王二彻底掌握,哇哥带着他手写一个基于 AQS 的独占锁 —— 不用 ReentrantLock,自己造一个,核心逻辑和 ReentrantLock 一样。

‼️步骤 1:自定义锁,继承 AQS

AQS 是抽象类,必须重写两个核心方法(这就是 “填充细节”):

  • tryAcquire(int arg):尝试拿锁(告诉 AQS 怎么判断能不能拿卡);
  • tryRelease(int arg):尝试释放锁(告诉 AQS 怎么还卡)。
package cn.tcmeta.aqs;


import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author: laoren
 * @description: // 基于AQS实现的独占锁
 * @version: 1.0.0
 */
public class TCLock implements Lock {
    // 核心:AQS的子类,重写核心方法
    private final Sync sync = new Sync();

    private static class Sync extends AbstractQueuedSynchronizer {
        // 1. 尝试获取锁(对应门禁刷卡)
        @Override
        protected boolean tryAcquire(int arg) {
            // 用CAS修改state:从0改成1(arg=1,代表获取1个锁资源)
            if (compareAndSetState(0, arg)) {
                // 记录当前获锁的线程(防止别人抢)
                setExclusiveOwnerThread(Thread.currentThread());
                return true; // 刷卡成功,进门
            }
            // 支持可重入:当前线程已经获锁,再次获取时state+1
            if (getExclusiveOwnerThread() == Thread.currentThread()) {
                setState(getState() + arg);
                return true;
            }
            return false; // 刷卡失败,去排队
        }

        // 2. 尝试释放锁(对应出门刷开)
        @Override
        protected boolean tryRelease(int arg) {
            // 只有获锁的线程能释放
            if (getExclusiveOwnerThread() != Thread.currentThread()) {
                throw new IllegalMonitorStateException("没刷卡的人不能开门!");
            }
            // 可重入释放:state减到0才真释放
            int newState = getState() - arg;
            boolean free = (newState == 0);
            if (free) {
                setExclusiveOwnerThread(null); // 清空获锁线程
            }
            setState(newState);
            return free; // 释放成功,通知下一个人
        }

        // 3. 判断是否持有锁(辅助方法)
        @Override
        protected boolean isHeldExclusively() {
            // 必须由当前线程持有且 state > 0 才算真正持有
            return getState() > 0 && getExclusiveOwnerThread() == Thread.currentThread();
        }

        // 生成Condition(对应VIP候场区)
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    // 以下是Lock接口的实现,全委托给Sync(AQS子类)
    @Override
    public void lock() {
        sync.acquire(1); // 调用AQS的acquire,触发tryAcquire+排队
    }

    @Override
    public void unlock() {
        sync.release(1); // 调用AQS的release,触发tryRelease+唤醒
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time)); // 正确传递超时时间
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public java.util.concurrent.locks.Condition newCondition() {
        return sync.newCondition();
    }

    private static volatile int stock = 10;
    private static volatile TCLock lock = new TCLock();

    /**
     * 扣减库存
     */
    private static void deductStock() {
        lock.lock();
        try {
            if (stock > 0) {
                stock--;
                System.out.println(Thread.currentThread().getName() + ":成功扣减库存,剩余:" + stock);
            } else {
                System.out.println(Thread.currentThread().getName() + ":库存不足");
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) { // 修改为主函数正确格式
        for (int i = 0; i < 10; i++) {
            new Thread(TCLock::deductStock, "线程" + i).start();
        }
    }
}

在这里插入图片描述
👉👉👉 核心亮点(哇哥划重点)

  • 我们只重写了 AQS 的tryAcquire和tryRelease,告诉 AQS “怎么抢锁”“怎么放锁”;
  • 线程排队、阻塞、唤醒的逻辑,全是 AQS 自带的,不用我们写一行 LockSupport 代码;
  • 支持可重入:同一线程多次 lock () 不会死锁,state 会累加,解锁时累减到 0 才释放。

六、AQS 的核心价值(王二记满小本本)

王二把 AQS 的核心价值总结成 3 句话,贴在显示器上:

  1. **标准化骨架:**把锁的 “排队、阻塞、唤醒” 这些重复逻辑抽成通用骨架,避免每个锁都重复造轮子;
  2. **灵活定制:**通过重写tryAcquire/tryRelease等方法,轻松实现独占锁(ReentrantLock)、共享锁(Semaphore);
  3. **线程安全:**内置 CAS、volatile 保证同步状态的线程安全,CLH 队列保证排队公平性。

📢 哇哥的血泪彩蛋

“我刚工作时,自己用 synchronized+wait 写锁,” 哇哥捂脸,“结果没处理好排队逻辑,线程经常‘插队’,库存超卖了好几千件 —— 后来换成 AQS 写的 ReentrantLock,只关心业务逻辑,再也没出过错。记住:不要和 AQS 抢活干,它比你懂并发!”

关注我,下一篇我们扒透 AQS 的 “手脚”——LockSupport,看看线程是怎么被精准阻塞和唤醒的,再手写个 Semaphore,让你彻底掌握 AQS 的实战用法!

请添加图片描述
请添加图片描述
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值