【034】AQS 高频深度面试题(附接地气详解)- 必背


以下面试题覆盖 AQS 核心考点,从基础概念到底层实现、实战应用,每道题都结合 “门禁系统” 等生活化例子拆解,答案既符合面试答题逻辑,又能体现技术深度,看完直接应对 90% 的 AQS 面试题!


在这里插入图片描述

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

AQS 高频深度面试题(附接地气详解)

✔️ 面试题 1:AQS 的全称和核心定位是什么?它解决了什么问题?

  • 问题级别:基础必问
  • 参考答案:

AQS 是AbstractQueuedSynchronizer(抽象队列同步器)的缩写,它不是锁,而是 Java 所有并发同步工具的 “通用骨架” —— 就像公司门禁的 “总控台”,本身不能开门,但定义了 “谁能进、谁排队、谁放行” 的核心规则。

它解决的核心问题:

避免重复造轮子:把 “线程排队、阻塞、唤醒、状态管理” 这些并发工具的通用逻辑抽成模板,ReentrantLock、Semaphore、CountDownLatch 等只需重写少量方法,不用重复实现底层逻辑;

保证线程安全:内置 CAS+volatile 管理同步状态(state),CLH 队列保证线程排队的公平性,解决了手动写锁时的线程安全问题;

统一阻塞 / 唤醒逻辑:基于 LockSupport 实现线程的精准阻塞和唤醒,比 wait/notify 更灵活,避免了 “空转耗 CPU”“唤醒盲盒” 等问题。

一句话总结

  • AQS 把并发同步的 “通用流程” 标准化,让开发者只需关注 “怎么抢锁、怎么放锁” 的业务逻辑,不用关心底层的排队和阻塞。

➡️ 面试题 2:AQS 的核心组成部分有哪些?CLH 队列的特点是什么?

  • 问题级别:核心必问
  • 详细答案

AQS 的核心由三部分组成,对应门禁系统的核心模块:

在这里插入图片描述
CLH 队列的核心特点(面试重点):

  • 双向链表 + FIFO:线程抢锁失败会被包装成 Node 节点,通过 CAS 加到队列尾部,唤醒时从队头开始,保证 “先来后到”;
  • 自旋 + 阻塞结合:节点入队后不会立即阻塞,先自旋尝试抢锁(减少上下文切换),自旋失败才调用 LockSupport.park () 阻塞;
  • 非阻塞设计:队列的入队 / 出队操作全靠 CAS 实现,不用加锁,保证高并发下的性能;
  • 节点状态标识:Node 节点有 CANCELLED、SIGNAL 等状态,标记线程是否取消、是否需要唤醒,避免无效的阻塞 / 唤醒。

举例子:
王二刷门禁卡(抢锁)失败,被包装成 Node 节点加到 CLH 队列尾部,队列头的李四刷完卡出门(释放锁)后,王二的节点变成队头,被 AQS 唤醒重新抢锁。

✅ 面试题 3:AQS 支持的独占锁和共享锁有什么区别?分别对应哪些并发工具?

  • 问题级别:核心必问
  • 详细答案:

AQS 的核心是通过重写不同的钩子方法,实现 “独占锁” 和 “共享锁” 两种模式,对应门禁的两种通行规则:

在这里插入图片描述
关键区别总结:

  • 独占锁的tryAcquire返回 boolean(成功 / 失败),共享锁的tryAcquireShared返回 int(>=0 = 成功,<0 = 失败);
  • 独占锁释放时只需唤醒一个线程,共享锁可能唤醒多个(比如读锁释放,所有等待读锁的线程都能获锁);
  • 独占锁存在 “锁竞争”,共享锁在许可充足时无竞争(比如 Semaphore 有 10 个许可,前 10 个线程直接获锁)。

👉 面试题 4:AQS 中线程的阻塞和唤醒依赖什么?LockSupport 和 wait/notify 的核心区别?

  • 问题级别:深度必问
  • 详细答案:

第一问:AQS 的线程阻塞 / 唤醒依赖 LockSupport

AQS 的 CLH 队列中,线程抢锁失败后,会调用LockSupport.park()阻塞;锁释放时,调用LockSupport.unpark(队头线程)精准唤醒 ——LockSupport 是 AQS 的 “手脚”,所有线程的阻塞 / 唤醒最终都靠它实现。

第二问:LockSupport vs wait/notify(面试高频对比)

用 “门禁对讲机” 和 “食堂喊号” 对比,核心区别如下:

在这里插入图片描述
举例子:

  • 用 wait/notify 时,食堂阿姨先喊 “来打饭”(notify),但食客还没到窗口(没调用 wait),食客到了后会一直等;
  • 用 LockSupport 时,保安先给王二开门禁权限(unpark),王二 2 秒后刷卡(park),依然能进门,不会白等。

✨ 面试题 5:AQS 的模板方法模式体现在哪里?为什么要设计成模板方法?

  • 问题级别:设计思想必问
  • 详细答案

第一问:模板方法模式的体现

AQS 是模板方法模式的经典应用,把方法分成两类:

  • 模板方法(已实现,不可重写):定义并发同步的通用流程,比如acquire()(抢锁流程)、release()(释放锁流程)、acquireShared()(共享锁抢锁流程)—— 这些方法内置了 “抢锁→入队→阻塞→唤醒” 的完整逻辑;
  • 钩子方法(空实现 / 默认实现,需子类重写):定义 “怎么抢锁、怎么放锁” 的具体规则,比如tryAcquire()、tryRelease()、tryAcquireShared()—— 子类(比如 ReentrantLock 的 Sync)只需重写这些方法,就能实现不同的锁逻辑。

模板方法流程示例(以独占锁 acquire 为例):

// AQS的模板方法(固定流程)
public final void acquire(int arg) {
    if (!tryAcquire(arg) && // 钩子方法:子类实现“怎么抢锁”
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { // 模板逻辑:入队+阻塞
        selfInterrupt();
    }
}

第二问:设计成模板方法的原因

  • 标准化流程:保证所有并发工具的底层逻辑一致(比如排队、阻塞规则),避免不同工具的 “逻辑碎片化”;
  • 降低开发成本:开发者只需关注业务逻辑(比如 ReentrantLock 的可重入规则、Semaphore 的许可规则),不用重复写底层的排队 / 阻塞代码;
  • 便于维护和扩展:AQS 的核心流程(比如 CLH 队列管理)集中维护,修改一处就能适配所有基于 AQS 的工具。

一句话总结:

  • 模板方法模式让 AQS“定流程、留接口”,既保证了底层逻辑的统一,又给上层工具足够的定制灵活性。

👉 面试题 6:ReentrantLock 的公平锁和非公平锁在 AQS 层面有什么区别?

  • 问题级别:实战必问

ReentrantLock 的公平 / 非公平锁,核心区别体现在 AQS 的tryAcquire方法(抢锁逻辑)上,对应门禁的 “排队规则”.

1. 公平锁(FairSync):严格按 CLH 队列顺序抢锁

抢锁时会先检查 CLH 队列是否有等待的线程,只有队头线程能抢锁 —— 就像门禁严格按排队顺序刷卡,不允许插队。

// 公平锁的tryAcquire核心逻辑
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 关键:先检查队列是否有等待线程,没有才抢锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 可重入逻辑(略)
    return false;
}

2. 非公平锁(NonfairSync):允许 “插队” 抢锁
抢锁时不检查队列,直接用 CAS 尝试抢锁,抢不到再进队列 —— 就像门禁允许 “没排队的人直接刷卡”,效率更高,但可能导致线程饥饿。

// 非公平锁的tryAcquire核心逻辑
protected final boolean tryAcquire(int acquires) {
    if (compareAndSetState(0, 1)) { // 关键:直接抢锁,不检查队列
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    // 可重入逻辑(略)
    return false;
}

核心区别总结:
在这里插入图片描述

‼️ 面试题 7:Semaphore 是如何基于 AQS 实现的?tryAcquireShared 返回值的含义?

  • 问题级别:深度必问
  • 详细答案

第一问:Semaphore 的 AQS 实现逻辑

Semaphore 是 AQS 共享锁的典型应用,核心是把 AQS 的 state 作为 “许可数”,对应门禁的 “通行权限数”(比如 10 个权限 = 一次能进 10 个人):

  • 初始化:new Semaphore(10) → AQS 的 state=10(10 个许可);
  • 获取许可(acquire ()):调用 AQS 的acquireShared(1),触发tryAcquireShared——CAS 把 state 减 1,剩余许可≥0 则获锁成功,否则进 CLH 队列阻塞;
  • 释放许可(release ()):调用 AQS 的releaseShared(1),触发tryReleaseShared——CAS 把 state 加 1,释放成功后唤醒队列中等待的线程。

核心源码(简化版):

// Semaphore的AQS子类
static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) { setState(permits); } // state=许可数
    // 共享锁抢许可
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            // 剩余许可<0则失败,否则CAS修改state
            if (remaining < 0 || compareAndSetState(available, remaining)) {
                return remaining;
            }
        }
    }
    // 共享锁释放许可
    protected boolean tryReleaseShared(int releases) {
        for (;;) {
            int available = getState();
            int next = available + releases;
            if (compareAndSetState(available, next)) {
                return true;
            }
        }
    }
}

核心源码(简化版):

// Semaphore的AQS子类
static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) { setState(permits); } // state=许可数
    // 共享锁抢许可
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            // 剩余许可<0则失败,否则CAS修改state
            if (remaining < 0 || compareAndSetState(available, remaining)) {
                return remaining;
            }
        }
    }
    // 共享锁释放许可
    protected boolean tryReleaseShared(int releases) {
        for (;;) {
            int available = getState();
            int next = available + releases;
            if (compareAndSetState(available, next)) {
                return true;
            }
        }
    }
}

第二问:tryAcquireShared 返回值的含义

tryAcquireShared的返回值是 “剩余许可数”,AQS 根据返回值判断线程是否需要排队:

  • 返回值 ≥ 0:抢许可成功,线程无需排队,直接执行;
  • 返回值 < 0:抢许可失败,线程进入 CLH 队列阻塞,等待其他线程释放许可。

举例子:
Semaphore 有 10 个许可,第 11 个线程调用 acquire (),tryAcquireShared返回 - 1,线程进入队列;当有线程释放许可后,state=1,唤醒队头线程,tryAcquireShared返回 0,线程获锁成功。

📌 面试题 8:AQS 如何处理 LockSupport 的虚假唤醒?

问题级别:深度必问
详细答案:

第一步:先明确 “虚假唤醒”

  • 虚假唤醒是指线程在没有被 unpark 的情况下,自己从 park () 中醒来 —— 就像门禁对讲机串台,没喊王二,但王二自己醒了过来。LockSupport 和 wait 都存在虚假唤醒,AQS 的解决思路是 “死循环 + 业务条件重试”。

第二步:AQS 的解决逻辑(核心在 acquireQueued 方法)

  • 队列中的线程被唤醒后,不会直接获锁,而是进入死循环,重新检查 “是否能抢锁”,就算虚假唤醒,也会重新判断业务条件:

核心逻辑总结:

  • 虚假唤醒后,线程回到死循环;
  • 重新检查 “是否是队头线程”+“是否能抢锁”(业务条件);
  • 若条件不满足,再次调用 LockSupport.park () 阻塞;
  • 只有条件满足,才退出循环,获锁成功。

一句话总结:
AQS 不试图避免虚假唤醒,而是用 “死循环 + 业务条件重试” 兜底,确保就算唤醒无效,线程也会重新检查条件,不会错误执行。

📢 面试题 9:AQS 的条件队列和 CLH 同步队列有什么区别?

问题级别:深度必问
详细答案:

AQS 有两个队列:CLH 同步队列(核心)和条件队列(配合 Condition),对应门禁的 “普通排队区” 和 “VIP 候场区”,核心区别如下:

在这里插入图片描述
举例子(ReentrantLock 的 Condition):

  • 王二拿到锁后,发现会议室没空(业务条件不满足),调用await()—— 王二的线程从 CLH 队列(已获锁,无节点)进入条件队列,释放锁;
  • 李四用完会议室,调用signal()—— 王二的线程从条件队列转移到 CLH 队列尾部;
  • 李四释放锁后,CLH 队列的王二被唤醒,重新抢锁,抢到后继续执行。

✔️ 面试题 10:如何基于 AQS 实现一个简易的 CountDownLatch?

  • 问题级别:手写实战必问
  • 详细答案:

CountDownLatch 是 AQS 共享锁的应用,核心是把 state 作为 “倒计时数”,state 减到 0 后,所有等待线程被唤醒 —— 就像门禁的 “集合锁”,等 10 个人到齐(countDown 10 次),才开门放行。

手写简易 CountDownLatch 代码(完整可运行):

package cn.tcmeta.aqs;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

import static java.lang.Thread.sleep;

/**
 * @author: laoren
 * @description: 自定义CountDownLatch
 * @version: 1.0.0
 */
public class MyCountDownLatch {
    // AQS共享锁子类
    private final Sync sync;

    // 构造方法:指定倒计时数(state=count)
    public MyCountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count不能为负");
        this.sync = new Sync(count);
    }

    // AQS核心逻辑:共享锁+倒计时
    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count); // state=倒计时数
        }

        // 共享锁抢锁逻辑:state=0才允许获锁
        @Override
        protected int tryAcquireShared(int acquires) {
            // state=0返回1(成功),否则返回-1(失败)
            return getState() == 0 ? 1 : -1;
        }

        // 共享锁释放逻辑:countDown一次,state减1
        @Override
        protected boolean tryReleaseShared(int releases) {
            for (; ; ) {
                int current = getState();
                if (current == 0) return false; // 已经减到0,无需操作
                int next = current - 1;
                if (compareAndSetState(current, next)) {
                    return next == 0; // 减到0时返回true,触发唤醒所有线程
                }
            }
        }
    }

    // 等待倒计时结束(调用AQS的共享锁抢锁)
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    // 倒计时减1(调用AQS的共享锁释放)
    public void countDown() {
        sync.releaseShared(1);
    }

    // 测试代码
    public static void main(String[] args) throws InterruptedException {
        MyCountDownLatch latch = new MyCountDownLatch(3); // 等3个线程完成

        // 启动3个工作线程
        for (int i = 1; i <= 3; i++) {
            int finalI = i;
            new Thread(() -> {
                System.out.println("线程" + finalI + ":开始工作");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("线程" + finalI + ":工作完成,倒计时减1");
                latch.countDown(); // 倒计时减1
            }).start();
        }

        // 主线程等待
        System.out.println("主线程:等待所有线程完成...");
        latch.await();
        System.out.println("主线程:所有线程完成,开始汇总结果");
    }
}

在这里插入图片描述
核心逻辑拆解:

  • 初始化:new MyCountDownLatch(3) → AQS 的 state=3;
  • 工作线程调用countDown() → tryReleaseShared把 state 减 1,减到 0 时返回 true,AQS 唤醒所有等待线程;
  • 主线程调用await() → tryAcquireShared检查 state 是否为 0,不为 0 则进入 CLH 队列阻塞,直到 state=0 才获锁成功。

面试答题技巧:

  • 手写时不用写完整代码,只需讲清 “state = 倒计时数”“tryAcquireShared 判断 state 是否为 0”“tryReleaseShared 减 state” 三个核心点,就能体现对 AQS 的掌握。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值