AQS学习

文章详细介绍了AQS(AbstractQueuedSynchronizer)框架的核心思想和实现机制,包括state的设计、线程阻塞恢复的park&unpark机制,以及基于CLH锁的队列设计。AQS通过FIFO队列管理线程等待,并提供了acquire和release方法来获取和释放资源。文章还详细分析了节点状态、节点的添加、获取资源及释放资源的源码流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、 AQS框架

AQS定义了一套多线程访问共享资源的同步器框架

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 实现的。CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。

 

牢记该图!!!

 AQS维护了一个共享资源state和一个FIFO的线程等待队列

1) state 设计

state 使用了 32bit int 来维护同步状态,state 使用 volatile 配合 cas 保证其修改时的原子性

2) 阻塞恢复设计

早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume那么 suspend 将感知不到

解决方法是使用 park & unpark 来实现线程的暂停和恢复

park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细park 线程还可以通过 interrupt 打断

3) 队列设计

使用了 FIFO 先入先出队列,并不支持优先级队列

设计时借鉴了 CLH 队列,它是一种单向无锁队列

二、源码

1. 节点状态

AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0新结点入队时的默认状态

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

2. acquire(int)

如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。

public final void acquire(int arg) {
    // 1. 尝试获取资源
    // 2. 获取失败创建新节点并将其加入等待队列尾部
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

2.1 tryAcquire(int)

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

具体如何尝试获取资源由自定义同步器实现。注意这里并不是abstract方法,因为只有独享模式才需要实现此方法。

2.2 addWaiter(Node)

private Node addWaiter(Node mode) {
    // 以mode模式构建节点,独占或者共享
    Node node = new Node(Thread.currentThread(), mode);
    // 尝试将node节点放到队尾
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //将tail更新为新加入的节点,即tail指向新入队节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 失败则通过enq入队
    enq(node);
    return node;
}

2.3 enq(Node)

private Node enq(final Node node) {
    for (;;) {// 自旋
        Node t = tail;
        if (t == null) { 
            // 如果tail为空,初始化一个节点将其设为head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 和addWaiter一样的流程
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

 2.3 acquireQueued(Node,int)

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; // 是否成功拿到资源
    try {
        boolean interrupted = false; // 是否被中断过
        for (;;) {
            final Node p = node.predecessor();
            // 如果前驱是head,说明有资格尝试获取资源
            if (p == head && tryAcquire(arg)) {
                // 获取到资源后,该节点就是第一个节点,因此设为head
                setHead(node);
                // 将p节点从队列中断开
                p.next = null; // help GC
                failed = false; // 获取资源成功
                return interrupted;
            }
            // 获取资源失败,尝试进入waiting
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

 2.3 shouldParkAfterFailedAcquire(Node pred, Node node)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前驱节点的状态已经是SIGNAL了
        return true;
    if (ws > 0) {
        // 前驱节点已取消。一直往前找,直到找到一个状态>0的节点,并将node作为该节点的下一节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 否则,将前驱的状态设为SIGNAL,这样可以在之后唤醒node
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

2.4 parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

2.5 总结

  1. 尝试获取资源,获取成功直接返回
  2. 获取资源失败,构建新节点并将其加入等待队列尾部
    1. 会依次尝试用两种方式入队
    2. 进入队尾后,acquireQueued()使线程在等待队列中休息,直到获取到资源后才返回。
      1. 自旋,在每一次旋转中
      2. 判断该节点是否是head的下一个节点,若是,尝试获取资源。
        1. 如果获取到资源,return。如果在整个等待过程中被中断过,则返回true,否则返回false。
        2. 如果获取资源失败,尝试进入waiting状态
          1. 从该节点node的前驱开始,找到waitStatus<0的第一个节点,并将该节点作为其下一个节点
          2. 将前驱的waitStatus的状态设为SIGNAL
          3. park()将当前线程进入waiting状态

 3. release(int)

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        // 找到head节点,并唤醒head的后继节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

3.1 tryRelease(int)

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

3.2 unparkSuccessor(Node)

唤醒等待队列中最前面的那个未放弃节点

// 唤醒node的后继节点
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) {
        // 如果下一个节点为空或者其waitStatus>0
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            // 从后往前寻找<=0的有效节点
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

这里需要注意两个地方:

  1. s被唤醒后,进入acquireQueued()if (p == head && tryAcquire(arg))的判断
    1. p为head
    2. p不为head。因为s已经是等待队列中最前边的那个未放弃线程,通过shouldParkAfterFailedAcquire()的调整,s必然会跑到head的next结点,下一次自旋p==head就成立)
    3. 然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()返回
  2. 为什么从后向前找,而不是从前往后找?

        由于并发问题,addWaiter()入队操作和cancelAcquire()取消排队操作都会造成next链的不一致,而prev链是强一致的,所以这时从后往前找是最安全的。

 

 而addWaiter()里每次compareAndSetTail(pred, node)之前都有node.prev = pred,即使compareAndSetTail失败,enq()会反复尝试,直到成功。一旦compareAndSetTail成功,该node.prev就成功挂在之前的tail结点上了,而且是唯一的,这时其他新结点的prev只能尝试往新tail结点上挂。

 

 

<think>嗯,用户之前问过JUC并发工具包,现在接着问AQS的实现原理,看来他们可能在学习Java并发编程,想要深入理解JUC的核心组件。AQS作为JUC中锁和同步器的基础框架,确实是一个关键点。用户可能是开发者,需要了解底层机制来优化代码或解决并发问题。 首先,我需要回忆AQS的基本结构,比如它如何通过一个int类型的state变量来表示资源状态,以及CLH队列的管理。用户可能想知道AQS是如何实现线程的阻塞和唤醒的,所以得解释Node节点和CAS操作的作用。同时,他们可能对独占模式和共享模式的区别感兴趣,需要分别说明。 然后,模板方法模式在AQS中的应用也很重要,用户可能想知道如何通过重写tryAcquire等方法来实现自定义同步器。比如ReentrantLock和Semaphore的不同实现方式,这时候举例子会帮助理解。 另外,用户可能对AQS中的公平和非公平模式有疑问,需要解释两者的区别,比如非公平模式如何减少线程切换开销。还有,如何处理中断和超时,这也是实际开发中常见的问题,得提到相关的方法如acquireInterruptibly。 在思考过程中,要确保覆盖AQS的核心设计,比如CLH变体队列、CAS操作、资源状态管理,以及具体的应用场景。同时,结合用户之前的JUC问题,可能需要将AQS与其他并发工具(如CountDownLatch、ReentrantLock)联系起来,说明它们如何依赖AQS实现。 最后,用户可能希望了解学习AQS的建议,比如阅读源码或调试示例,这部分需要给出实用的建议,帮助他们进一步掌握。要避免过于技术化的术语,用易懂的语言解释清楚原理和实现细节,确保回答结构清晰,逐步深入,满足用户的需求。</think>AQS(AbstractQueuedSynchronizer)是Java并发包的核心框架,JUC中大多数同步工具(如ReentrantLock、CountDownLatch等)都基于它实现。其核心原理可分为以下关键部分: --- ### **一、核心数据结构** 1. **state变量(volatile int)** - 表示同步状态的核心字段 - 不同工具类的语义不同: $$ \begin{cases} \text{ReentrantLock: 锁重入次数} \\ \text{Semaphore: 剩余许可数} \\ \text{CountDownLatch: 剩余计数器} \end{cases} $$ 2. **CLH队列变体** - 使用双向链表实现的等待队列(FIFO) - 每个节点对应等待线程(Node内部类) --- ### **二、关键工作机制** #### 1. **资源访问控制** - **独占模式**(Exclusive) ```java // 如ReentrantLock acquire(int arg) → tryAcquire(arg) → addWaiter() → acquireQueued() ``` - **共享模式**(Shared) ```java // 如Semaphore acquireShared(int arg) → tryAcquireShared(arg) → doAcquireShared() ``` #### 2. **线程阻塞/唤醒** - 通过`LockSupport.park()`/`unpark()`实现 - 当线程获取资源失败时,构建Node加入队列并阻塞 - 前驱节点释放资源时触发唤醒 --- ### **三、核心方法实现** #### 1. **获取资源(以独占模式为例)** ```java public final void acquire(int arg) { if (!tryAcquire(arg) && // 尝试直接获取(需子类实现) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入队列并等待 selfInterrupt(); } ``` #### 2. **释放资源** ```java public final boolean release(int arg) { if (tryRelease(arg)) { // 尝试释放(需子类实现) Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒后继节点 return true; } return false; } ``` --- ### **四、关键技术实现** #### 1. **CAS操作** - 通过Unsafe类实现队列节点操作: $$ \text{compareAndSetHead(Node update)} $$ $$ \text{compareAndSetTail(Node expect, Node update)} $$ #### 2. **队列同步策略** - **自旋检测**:队列中线程不断检查前驱节点状态 - **取消机制**:超时/中断时移除无效节点 - **头节点唤醒**:仅头节点有资格尝试获取资源 --- ### **五、设计模式** 1. **模板方法模式** - 需要子类实现的关键方法: $$ \begin{cases} \text{tryAcquire(int)} \\ \text{tryRelease(int)} \\ \text{tryAcquireShared(int)} \\ \text{tryReleaseShared(int)} \end{cases} $$ 2. **公平/非公平策略** - 公平锁:严格按照队列顺序获取 - 非公平锁:允许插队(减少上下文切换) --- ### **六、典型实现对比** | 同步器 | 使用模式 | state语义 | 关键方法实现 | |----------------|-----------|------------------|---------------------------| | ReentrantLock | 独占 | 重入次数 | tryAcquire()检查当前线程是否持有 | | Semaphore | 共享 | 可用许可数 | tryAcquireShared()扣减许可数 | | CountDownLatch | 共享 | 剩余计数器 | tryReleaseShared()递减计数 | --- ### **七、源码级关键细节** 1. **节点状态(waitStatus)** ```java static final int CANCELLED = 1; // 线程已取消 static final int SIGNAL = -1; // 需要唤醒后继节点 static final int CONDITION = -2; // 在条件队列中等待 ``` 2. **资源获取的两种方式** - **不可中断模式**:acquire() - **可中断模式**:acquireInterruptibly() --- ### **八、最佳实践建议** 1. 优先使用现有的JUC工具类,避免直接继承AQS 2. 实现自定义同步器时: - 准确维护state状态 - 正确处理线程中断 - 合理选择公平/非公平策略 3. 使用`Condition`对象时注意与锁的绑定关系 --- ### **学习建议** 1. 从`ReentrantLock`源码入手理解独占模式 2. 使用IDEA调试线程排队/唤醒过程 3. 通过`AbstractQueuedSynchronizer`的Javadoc理解设计思想 AQS通过精妙的队列管理和状态控制,实现了高性能的同步机制,是理解Java并发编程的核心基石。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值