AQS(AbstractQueuedSynchronizer)抽象队列同步器

前言

AQS 绝对是JUC的重要基石,也是面试中经常被问到的,所以我们要搞清楚这个AQS到底是什么?骑工作原理是什么?

AQS是什么?

是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO对列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。
在这里插入图片描述
CLH队列:CLH(Craig, Landin, and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。通过CAS完成对State值的修改。
同步对列的内部结构及继承关系
在这里插入图片描述
在这里插入图片描述

用银行办理业务的案例模拟AQS如何进行线程管理和通知机制

1、初始化的时候,state = 0 (0 表示没有人,1表示有人),线程池也有没有人在执行,现在就是没有顾客的时候,因此第一个线程去的时候都是公平锁状态直接到窗口办理业务。
在这里插入图片描述
2、现在有一个线程Thread A进来那么就直接到了业务窗口去办理,并且通过CAS将state的值变成1
在这里插入图片描述
3、此时有一个线程Thread B 进来,通过getStatue() 方法查看到state = 1,此时ThreadA有在占用着,所以现在ThreadB线程就必须先入队等待ThreadA结束后再调用,但是由于现在队列是空的,所以ThreadB线程并不会马上进入到队列,他会先进入addWaiter() 方法到enq()这个方法中去,这个方法实质上就是一个自旋锁,在这个方法中主要的时候先要实现一个队列的初始化工作,先形成一个傀儡结点(哨兵结点)起到一个占位的作用,然后才能将ThreadB线程挂在后面。
在这里插入图片描述
部分源码附上:

	private Node addWaiter(Node mode) {
     Node node = new Node(Thread.currentThread(), mode);
     // Try the fast path of enq; backup to full enq on failure
     Node pred = tail;// 将tail的前指针赋值过去
     if (pred != null) { //一开始的时候并没有指向所以位null,因此条件不成立不走里面的语句
         node.prev = pred;
         if (compareAndSetTail(pred, node)) {//CAS
             pred.next = node;
             return node;
         }
     }
     enq(node);//一开始会来走这个enq()这个方法。
     return node;
 }

因为当前的ThreadB线程进入时,tail的prev指针为null所以就会进enq()方法。如下:

	private Node enq(final Node node) {
     for (;;) {
         Node t = tail;
         if (t == null) { // Must initialize
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else {
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
 }

4、ThreadC线程开始入队,那么这个就是跟ThreadB是一样的,只不过是说没有了初始化那步了,直接挂在线程ThreadB上即可。
在这里插入图片描述
5、ThreadB 会去尝试acquireQueued()这个方法,那么第一次的时候会将哨兵结点的waitStatus = -1; 然后继续自旋,进入到if条件的下一个条件中被调用park() 阻塞掉,等待着ThreadA 线程的unpark()。这样才算真正的入列等待了。
在这里插入图片描述

	final boolean acquireQueued(final Node node, int arg) {
     boolean failed = true;
     try {
         boolean interrupted = false;
         for (;;) {
             final Node p = node.predecessor();
             if (p == head && tryAcquire(arg)) {
                 setHead(node);
                 p.next = null; // help GC
                 failed = false;
                 return interrupted;
             }
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt()) //第一次的时候会阻塞在前面的条件并将哨兵的waitState置为-1,第二次的话第一个条件为真,所以会走第二个判断条件,会将访问的线程给阻塞掉等待unpark();
                 interrupted = true;
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }
	private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this); //将访问次方法的线程给阻塞掉,等待unpark()方法唤醒
     return Thread.interrupted();
 }

6、当ThreadA线程办好业务的时候那么就会调用unlock()方法释放锁,unlock() 方法中又会调用sync.release()方法,并将当前窗口的线程变成null,state 置成0,通过调用 unparkSuccessor()方法将 傀儡结点的 waitState = 0
在这里插入图片描述

	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); //释放锁唤醒等待队列中的等待线程
    }

7、接着会调用unpark()方法将在前面等待的ThreadB线程给唤醒去窗口办业务。将head结点指向ThreadB结点,ThreadB的prev结点为null,next结点也为null,ThreadB变成空结点,此节点就成了新的哨兵结点,原来的哨兵结点被GC回收。
在这里插入图片描述
8、ThreadC线程出列和上面的过程是一样的。

结语

AQS的核心大致是这样,如果说我讲的你还是没有听懂的话,可以去B站看善硅谷阳哥的高频面试题那集去看看。我只是将其做了一个简化,用自己的话把这个过程复述出来,希望能对你有所帮助。
收工~

### 抽象队列同步器 AQS 的原理与实现 #### 1. AQS 的定义与作用 AQSAbstractQueuedSynchronizer)是 Java 并发包 `java.util.concurrent` 中的一个核心组件,它提供了一种框架化的机制来构建锁和其他同步工具。AQS 使用了一个基于 FIFO 的双向链表队列来管理线程之间的竞争关系,并通过状态变量控制线程的同步行为[^1]。 #### 2. 核心组成部分 ##### (1)状态变量(state) AQS 维护一个名为 `state` 的整型变量,用来表示同步状态。不同的子类可以通过原子操作修改这个变量以反映资源的占有情况。例如,在 ReentrantLock 中,`state` 表示当前锁被持有的次数;而在 Semaphore 中,`state` 则代表剩余信号量的数量[^3]。 ##### (2)CLH 队列 为了高效地处理大量线程的竞争,AQS 内部采用 CLH(Craig, Landin, and Hagersten locks)变体形式的队列结构。当一个线程试图获取锁失败时,会被封装成节点加入到该队列中等待下一次机会。每个节点包含了指向前后相邻节点的指针以及一些标志位用于记录自身的状况[^4]。 #### 3. 主要方法分类 根据职责划分,AQS 定义了一系列模板方法供开发者覆盖实现: - **独占模式 vs 共享模式** - 独占模式意味着同一时刻只有一个线程能成功获得许可执行关键路径上的代码片段。 - 共享模式允许多个线程同时持有相同的权限级别而不违反约束条件。 - **tryAcquire/tryRelease 系列函数** 这些是非公平版本下的尝试性接口,默认情况下不考虑排队顺序直接抢夺资源使用权。如果调用者满足准入资格则返回 true ,否则 false 。对于释放动作而言则是更新内部计数器并将可能唤醒下一个候选者[^2]。 - **acquire/acquireShared 方法族** 正常途径进入临界区的方式分为两类:一是完全遵循既定规则逐级向上申请直至达成目的为止;二是允许一定程度上的投机取巧即所谓的“乐观锁定策略”。 - **release/releaseShared 函数组** 类似于上面提到的内容只不过方向反转过来而已——前者是从拥有权角度出发逐步削减直到彻底放弃掌控权;后者强调集体利益最大化原则之下共同退让部分权益给后来者享用[^2]。 #### 4. 锁定机制详解 以下是有关如何运用 AQS 构建自定义同步原语的一些简单例子: ```java public class MyMutex extends AbstractQueuedSynchronizer { protected boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { // CAS 更新 state 值 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setState(0); // 清零恢复初始态 setExclusiveOwnerThread(null); // 解绑当前所有者信息 return true; // 返回值决定是否通知后续待命成员 } public void lock() { acquire(1); } public void unlock() { release(1); } } ``` 上述代码展示了怎样借助 AQS 来模拟互斥锁的行为特性。其中重点在于重写两个虚基类提供的纯虚函数分别对应加锁解锁两阶段的操作逻辑[^3]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

聪明不喝牛奶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值