欢迎光临
我们一直在努力

解析AQS:Java并发编程的“万能骨架“

欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。

如果你还在为Java并发编程中的锁机制感到困惑,如果你在面试中被问到"ReentrantLock底层原理"时支支吾吾,那么这篇文章就是为你准备的。今天,我们来揭开AQS的神秘面纱,理解这个支撑整个Java并发生态的"万能骨架"。

1. 为什么AQS是Java并发的"地基"?

想象一下,你正在使用ReentrantLock加锁、用CountDownLatch等待线程结束、用Semaphore控制并发数——这三个工具功能完全不同,但它们的底层代码都绕不开同一个核心组件:AQS(AbstractQueuedSynchronizer,抽象队列同步器)。

AQS就像盖房子时的钢筋混凝土地基,无论是别墅还是公寓,都离不开这个基础。它定义了锁竞争、线程排队、唤醒的核心规则,让上层工具只需实现个性化逻辑,无需重复造轮子。

看看这些简化源码片段,你会发现惊人的相似性:

// ReentrantLock的lock()方法,底层调用AQS的acquire()
public void lock() {
    sync.acquire(1);
}

// CountDownLatch的await()方法,底层调用AQS的acquireShared()
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// Semaphore的acquire()方法,底层调用AQS的acquireShared()
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

无论是独占锁(ReentrantLock)还是共享锁(CountDownLatch、Semaphore),核心操作都委托给了AQS。这就是AQS的核心价值:封装并发编程的共性问题,让上层工具只需关注个性化逻辑。

2. 用"生活场景"看懂AQS的核心原理

光说概念太抽象,我们用"抢厕所"的场景来类比,一下子就能懂:

假设公司只有1个厕所(对应"1把锁"),门口挂着一块牌子(对应AQS的state状态变量):

  • 牌子写"0":厕所空闲(锁可用)

  • 牌子写"1":厕所占用(锁已被拿)

2.1 线程抢锁:能拿就拿,拿不到排队

当线程A来抢锁时,会先看门口的牌子(state):

  • 如果是"0":直接把牌子改成"1",然后进厕所(成功获取锁)

  • 如果是"1":没法进,就乖乖站到门口的队伍里(进入AQS的等待队列),并且自己"休眠"(阻塞),等里面的人出来了再喊它

这里有个关键细节:改牌子(state)的操作是"原子的"——就像只有一个人能同时改牌子,不会出现两个线程都看到"0",都改成"1"的情况(这靠CAS操作保证)。

2.2 线程释放锁:改回牌子,喊下一个人

当里面的线程A用完厕所(释放锁),会做两件事:

  • 把门口的牌子改回"0"(state重置)

  • 从队伍里喊第一个人(唤醒等待队列的头节点线程),让它来抢锁

这样一来,整个抢锁、排队、释放的流程就顺畅了,不会乱套。

3. AQS的两个核心"零件"

想彻底理解AQS,必须记住它的两个核心组成部分,这也是子类实现的关键:

ReentrantLock 
state—I 
rev 
park 
2.j0üR-n 
park 
AQS 
next 
SIGNAL 
head 
rev 
next 
SIGNAL 
next 
rev 
tail 
3.ÄßÅ51J 
5.ÄßÅ51J

3.1 状态变量(state):锁的"晴雨表"

前面类比的"牌子"就是state,它是一个volatile int变量(保证线程间可见性),不同的子类会赋予它不同的含义:

  • ReentrantLock(可重入锁):state=0表示无锁,state>=1表示有锁(重入一次加1,释放一次减1,减到0才真释放)

  • Semaphore(信号量):state表示"可用的资源数量",线程抢资源时state减1,释放时加1

  • CountDownLatch(倒计时器):state表示"倒计时次数",调用countDown()就减1,减到0时唤醒所有等待线程

AQS提供了3个方法操作state,子类可以直接用:

  • getState():获取当前状态

  • setState(int newState):设置新状态

  • compareAndSetState(int expect, int update):CAS方式改状态(保证原子性,核心中的核心)

3.2 等待队列(CLH队列):线程的"候客厅"

当线程抢不到锁时,会被包装成一个"节点"(Node),加入到一个双向链表里,这就是CLH队列(以发明者Craig、Landin和Hagersten的名字命名)。

这个队列有两个特点:

  • 先进先出(FIFO):先排队的线程先被唤醒(公平锁的核心逻辑)

  • 带头节点的链表:头节点是"正在用锁的线程",后面的节点是"等待的线程",释放锁时只需要唤醒头节点的下一个节点就行,效率高

每个Node节点包含:

  • 等待的线程(thread)

  • 前驱节点(prev)、后继节点(next)

  • 等待状态(waitStatus):比如"等待中(SIGNAL)"、"取消等待(CANCELLED)"

4. AQS的两种工作模式

AQS支持两种同步模式,适应不同的并发场景:

4.1 独占模式(Exclusive)

特点:同一时间只有一个线程能获取资源,就像卫生间只能一个人用。

应用场景:ReentrantLock、ReentrantReadWriteLock.WriteLock

核心方法:

// 获取资源,失败则进入队列阻塞
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

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

4.2 共享模式(Shared)

特点:多个线程可同时获取资源,就像电影院可以多人同时入场,但有座位数限制。

应用场景:Semaphore、CountDownLatch

核心方法:

// 共享式获取资源
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

// 共享式释放资源,传播唤醒信号
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

5. 深入源码:AQS如何实现高性能加锁

AQS底层加锁、释放锁,都是大量基于CAS操作来实现的。让我们看看ReentrantLock的非公平锁实现:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

关键点解析:

  • compareAndSetState(0, 1):判断state是否为0,如果是代表没人加过锁,把这个state设置为1

  • CAS可以无锁化保证一个数值修改的原子性,底层基于Unsafe实现,通过CPU指令实现原子性的CAS操作

  • 如果CAS成功,设置当前线程为独占锁持有者;如果失败,调用acquire(1)进入排队等待

5.1 可重入锁的实现原理

如果同一个线程可重入加锁,compareAndSetState方法返回一定是false,此时会执行acquire(1),最终进入nonfairTryAcquire方法:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();

    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 关键:判断当前线程是否已经是锁持有者
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;  // 累加state值
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可重入机制:当线程尝试获取锁时,如果发现state不为0,但当前线程就是锁持有者,就直接将state值累加,代表重入次数增加。释放锁时,每次释放将state减1,直到减到0才真正释放锁。

6. 线程排队与唤醒的完整流程

当线程加锁失败时,AQS会执行以下完整流程:

6.1 封装节点并入队

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)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);  // 完整入队操作
    return node;
}

6.2 阻塞等待

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())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

6.3 释放锁并唤醒

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

7. 公平锁 vs 非公平锁:性能与公平的权衡

AQS支持公平锁和非公平锁两种策略,这是面试中的高频考点:

7.1 公平锁(FairSync)

核心特点:严格按照FIFO顺序让队列中的线程获取锁。

关键代码:

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;
        }
    }
    // … 重入逻辑
}

hasQueuedPredecessors()方法会检查队列中是否有比当前线程更早等待的线程,有就返回true,当前线程就不会去抢锁,保证了公平性。

7.2 非公平锁(NonfairSync)

核心特点:允许新来的线程"插队",与队列头节点唤醒的线程竞争。

性能优势:非公平锁在tryAcquire时没有hasQueuedPredecessors()判断,直接尝试CAS抢锁,减少了性能开销。根据测试,在并发量10000时,非公平锁的吞吐量比公平锁高了约20%。

线 程 4 
线 程 1 
线 程 2 
park 
线 程 挂 起 
线 程 3 
在 线 程 1 释 放 锁 的 瞬 间 
尝 试 加 锁 , 加 锁 成 功 
放 锁 
1 、 加 锁 
state 为 0 可 以 
加 锁 成 功 
2 . 加 锁 失 败 
入 队 列 等 待 
park 
线 程 挂 起 
4 . 加 锁 失 败 
入 队 列 等 待 
ReentrantLock 
state—I 
rev 
AQS 
next 
SIGNAL 
head 
rev 
next 
SIGNAL 
next 
rev 
0 
tail 
当 前 加 锁 线 程 = 线 程 4 
3 . 入 队 列 
阻 塞 等 待 
5 . 入 队 列 
阴 塞 等 待

选择建议:

  • 非公平锁:适用于任务执行时间短的场景,减少线程切换开销

  • 公平锁:适用于需要避免饥饿的敏感任务,如交易系统

8. 读写锁:读多写少场景的优化

ReentrantReadWriteLock是AQS的另一个经典应用,它巧妙利用state变量的高低16位分别表示读锁和写锁:

  • 高16位:读锁计数(允许多个线程同时读取)

  • 低16位:写锁计数(同一时间只能有一个线程写入)

ReentrantReadWriteLock 
AbstractQueueSynchronizer 
state= I 
ReadLock 
WriteLock

8.1 读写锁的互斥规则:

  • 读锁 → 写锁:互斥(有读锁时不能加写锁)

  • 写锁 → 读锁:互斥(有写锁时不能加读锁)

  • 写锁 → 写锁:互斥(同一时间只能有一个写锁)

  • 读锁 → 读锁:不互斥(允许多个读锁同时存在)

这种设计在读多写少的场景下能显著提升并发性能,如缓存系统、配置表等。

XWGßÅ51J 
GEI 
ReadLock 
WriteLock 
ReentrantReadWriteLock 
AbstractQueueSynchronizer 
state= 1 
Node 
next 
waitStatus=- 1 
rev 
Node 
next 
waitStatus=- 1 
head 
rev 
next 
waitStatus=O

9. AQS的性能优化策略

现代AQS实现包含多项优化策略,确保在高并发场景下的性能:

9.1 快速路径(Fast Path)

在acquire()中先直接尝试获取锁,避免不必要的入队操作。

9.2 自旋优化

线程在阻塞前进行有限次自旋,减少上下文切换开销。

9.3 队列跳跃

addWaiter()中的CAS快速入队尝试,提高入队效率。

9.4 唤醒传播

共享模式下的doReleaseShared()级联唤醒,提高唤醒效率。

9.5 避免伪共享

通过缓存行填充优化节点内存布局,减少CPU缓存失效。

10. 如何基于AQS实现自定义同步器?

AQS采用模板方法模式,让开发者可以轻松实现自定义同步器。以下是实现步骤:

10.1 继承AQS

创建一个新的类,继承AbstractQueuedSynchronizer。

10.2 定义同步状态

根据需求定义state变量的语义。

10.3 实现核心方法

根据同步器类型实现相应方法:

独占模式:

protected boolean tryAcquire(int arg) {
    // 定义获取独占锁的逻辑
}

protected boolean tryRelease(int arg) {
    // 定义释放独占锁的逻辑
}

共享模式:

protected int tryAcquireShared(int arg) {
    // 定义获取共享锁的逻辑
}

protected boolean tryReleaseShared(int arg) {
    // 定义释放共享锁的逻辑
}

10.4 提供外部接口

实现Lock或其他接口,对外提供加锁、解锁等方法。

11. AQS vs Synchronized:如何选择?

特性

AQS(如ReentrantLock)

Synchronized

实现方式

Java代码实现

JVM内置实现

锁获取方式

可中断、可超时

不可中断

公平性

可支持公平/非公平

非公平

条件队列

支持多个条件

单个条件

性能

高竞争下表现更好

低竞争下更优

选择建议:

  • ReentrantLock优先:高竞争(线程数>32)、需要超时/中断/公平性、有并发编程经验

  • Synchronized优先:低竞争(线程数<8)、基础同步需求、新手团队或快速开发

12. 总结:AQS的设计哲学

AQS的成功在于其"抽象与复用"的思想:

  • 标准化同步框架:将复杂线程调度抽象为状态管理与队列操作

  • 高性能保证:通过CAS和volatile实现无锁化竞争

  • 灵活扩展:模板方法模式支持多样化同步器实现

理解AQS不仅有助于我们更好地使用Java并发工具,更能提升构建高性能、高可靠性并发组件的能力。正如AQS的设计者Doug Lea所言:"并发问题的本质是对资源的合理调度"。

赞(0)
未经允许不得转载:171主机测评 » 解析AQS:Java并发编程的“万能骨架“
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址