《面试官问我Java中的AQS是啥,我这样答他当场满意》

大家好呀!今天我们要聊的是Java并发编程中一个超级重要的"幕后大佬"——AbstractQueuedSynchronizer(简称AQS)🤵。别看它名字又长又拗口,它可是Java并发包的核心基础,像ReentrantLock、CountDownLatch这些我们常用的工具类,都是基于它实现的哦!✨

一、AQS是什么?🤔

AQS就像是一个万能并发控制器🔧,它提供了一套框架,让开发者可以轻松实现各种同步工具。想象一下,你去银行办业务,AQS就是那个管理排队叫号系统的机器📟,它知道现在谁在办理业务(持有锁),谁在排队(等待队列),以及什么时候该叫下一个号(线程唤醒)。

1.1 为什么需要AQS?

在没有AQS之前,每个同步工具都要自己实现:

  • 如何管理线程的排队
  • 如何实现线程的阻塞和唤醒
  • 如何处理各种边界条件

这就像每家银行都要自己发明一套排队系统,多麻烦啊!😫 AQS的出现,把这些公共逻辑都封装好了,我们只需要告诉它"什么时候能获取锁"、"什么时候要释放锁"就行了。

1.2 AQS的核心思想

AQS的核心可以用三句话概括:

  1. 状态管理:用一个volatile的int变量表示同步状态🔢
  2. 排队机制:用CLH队列管理等待线程🚶‍♂️🚶‍♀️🚶‍♂️
  3. 模板方法:留出关键方法让子类实现✂️

二、AQS的底层原理🔧

2.1 状态变量 - 同步的核心

AQS内部维护了一个重要的变量:

private volatile int state;

这个state就像银行窗口的"使用状态":

  • state = 0:窗口空闲🟢
  • state > 0:窗口被占用🔴

不同的同步工具对state的解释不同:

  • 对于锁:state表示重入次数
  • 对于信号量:state表示剩余许可数
  • 对于CountDownLatch:state表示还需要countDown的次数

2.2 CLH队列 - 线程排队的地方

当线程获取同步状态失败时,AQS会把线程包装成一个Node节点,加入CLH队列。这个队列有以下几个特点:

  1. 双向链表:每个节点都知道它的前驱和后继
  2. 自旋+CAS:通过CAS保证线程安全
  3. 公平性支持:可以实现公平和非公平两种模式
static final class Node {
    volatile Node prev;    // 前驱节点
    volatile Node next;    // 后继节点
    volatile Thread thread; // 等待的线程
    int waitStatus;        // 等待状态
}

2.3 关键方法 - 子类需要实现的

AQS使用了模板方法模式,留了几个关键方法让子类实现:

  1. tryAcquire(int arg):尝试获取锁🔒
  2. tryRelease(int arg):尝试释放锁🔓
  3. tryAcquireShared(int arg):共享式获取
  4. tryReleaseShared(int arg):共享式释放

这就像银行给你一个排队系统的框架,你只需要定义:

  • 什么条件可以办理业务(tryAcquire)
  • 什么条件算业务办完了(tryRelease)

三、AQS的工作原理详解⚙️

3.1 独占模式(如ReentrantLock)

场景:只有一个窗口(资源),多个人要办理业务

获取锁流程🔒:
  1. 线程调用acquire(1)尝试获取锁
  2. AQS先调用子类的tryAcquire(1)方法
    • 成功:直接返回,线程继续执行
    • 失败:进入下一步
  3. 创建Node节点,加入CLH队列尾部
  4. 进入自旋状态,不断检查:
    • 自己是头节点的下一个节点吗?
    • 再次尝试tryAcquire(1)能成功吗?
  5. 如果满足条件,获取锁成功;否则被挂起
释放锁流程🔓:
  1. 线程调用release(1)释放锁
  2. AQS调用子类的tryRelease(1)方法
  3. 如果释放成功,唤醒队列中的下一个线程
  4. 被唤醒的线程重新尝试获取锁

3.2 共享模式(如CountDownLatch)

场景:多个线程可以同时访问资源,比如景区限流

获取共享资源流程👥:
  1. 线程调用acquireShared(1)尝试获取
  2. AQS调用子类的tryAcquireShared(1)
    • 返回值≥0:获取成功
    • 返回值<0:获取失败
  3. 失败后加入队列,等待被唤醒
释放共享资源流程🎉:
  1. 线程调用releaseShared(1)释放资源
  2. AQS调用子类的tryReleaseShared(1)
  3. 如果释放成功,唤醒队列中的等待线程

四、AQS在JDK中的应用实例🌈

4.1 ReentrantLock

ReentrantLock是AQS最典型的应用,它实现了公平锁和非公平锁两种模式:

// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {  // 锁空闲
        if (compareAndSetState(0, acquires)) {  // CAS抢锁
            setExclusiveOwnerThread(current);   // 设置持有线程
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {  // 重入
        setState(c + acquires);
        return true;
    }
    return false;
}

4.2 CountDownLatch

CountDownLatch用于让一组线程等待直到计数变为0:

// 共享式获取
public int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

// 共享式释放
public boolean tryReleaseShared(int releases) {
    for (;;) {  // 自旋CAS
        int c = getState();
        if (c == 0) return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))  // CAS减1
            return nextc == 0;  // 返回是否减到0了
    }
}

4.3 Semaphore

Semaphore用于控制同时访问的线程数量:

// 获取许可
protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || 
            compareAndSetState(available, remaining))
            return remaining;
    }
}

五、AQS源码深度解析🔬

5.1 节点状态详解

AQS中的Node节点有几种重要状态:

// 取消状态
static final int CANCELLED =  1;  
// 等待触发状态(后继节点需要被唤醒)
static final int SIGNAL    = -1;  
// 等待条件状态
static final int CONDITION = -2;  
// 共享模式下传播状态
static final int PROPAGATE = -3;  

5.2 入队操作源码

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 快速尝试在尾部添加
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {  // CAS设置尾节点
            pred.next = node;
            return node;
        }
    }
    enq(node);  // 快速添加失败,进入完整入队流程
    return node;
}

private Node enq(final Node node) {
    for (;;) {  // 自旋直到成功
        Node t = tail;
        if (t == null) {  // 队列为空,初始化
            if (compareAndSetHead(new Node()))  // 设置哑节点
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

5.3 出队与唤醒

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);  // 清除状态
    
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {  // 下一个节点取消或为空
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)  // 从后向前找第一个有效节点
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);  // 唤醒线程
}

六、AQS使用注意事项⚠️

6.1 正确实现模板方法

实现AQS时要注意:

  1. tryAcquire/tryRelease要保证线程安全
  2. 状态变更要使用getState()/setState()/compareAndSetState()
  3. 考虑可重入性

6.2 避免死锁

使用AQS实现的锁要注意:

  1. 获取锁和释放锁要成对出现
  2. 在finally块中释放锁
  3. 避免锁嵌套导致死锁

6.3 性能考量

  1. 非公平锁通常性能更好
  2. 尽量减少临界区代码
  3. 考虑使用读写锁替代独占锁

七、手写一个基于AQS的锁🔐

让我们用AQS实现一个最简单的锁:

public class MyLock {
    private final Sync sync = new Sync();
    
    public void lock() {
        sync.acquire(1);
    }
    
    public void unlock() {
        sync.release(1);
    }
    
    private static class Sync extends AbstractQueuedSynchronizer {
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, 1);  // CAS将0改为1
        }
        
        protected boolean tryRelease(int arg) {
            setState(0);  // 直接设置为0
            return true;
        }
        
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }
}

这个简单的锁虽然功能有限,但已经具备了基本的互斥功能!

八、AQS常见面试题💡

8.1 AQS为什么使用CLH队列?

  1. 减少争用:CLH队列每个节点只需要轮询前驱的状态
  2. 内存高效:节点只需要关注前驱,不需要全局同步
  3. 公平性:天然支持FIFO公平策略

8.2 AQS如何保证可见性?

  1. state使用volatile修饰
  2. 节点中的waitStatus也是volatile的
  3. 使用Unsafe类提供的CAS操作

8.3 AQS为什么要有头尾指针?

  1. 头指针:指向当前持有锁的节点
  2. 尾指针:方便快速入队
  3. 双向链表:方便取消操作时快速移除节点

九、AQS性能优化技巧🚀

9.1 减少CAS竞争

  1. 使用多个状态变量分散竞争
  2. 考虑使用分层锁设计
  3. 适当增加自旋次数减少上下文切换

9.2 避免不必要的阻塞

  1. 先快速尝试获取锁
  2. 使用tryLock()替代阻塞获取
  3. 设置合理的超时时间

9.3 选择合适的同步工具

  1. 读多写少用ReadWriteLock
  2. 并发计数用CountDownLatch
  3. 资源池管理用Semaphore

十、总结🎯

AQS是Java并发包的基石,理解AQS的运作原理:

  1. 掌握了状态管理的核心思想
  2. 理解了CLH队列的排队机制
  3. 学会了模板方法的设计模式应用

下次当你使用ReentrantLockCountDownLatch这些工具时,就能想象到它们背后AQS这位"幕后大佬"是如何工作的了!🤵💼

希望这篇超详细的AQS解析能帮到你!如果有任何问题,欢迎在评论区留言讨论哦~💬😊

记住:并发编程就像管理一个高效的团队,AQS就是那个确保每个人都按规则行事,又能最大限度发挥效率的团队管理者!👨‍💼👩‍💼

Happy coding! 🚀💻

推荐阅读文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值