终于知道AQS是啥东西了

终于知道aqs是啥东西了

java 复制代码
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

AQS 是 AbstractQueuedSynchronizer 的缩写,藏在 java.util.concurrent.locks 包下面。它不是锁,不是工具类,是造锁用的模板------子类只负责回答一个问题:"我现在能抢到吗?",剩下的排队、入队、阻塞、唤醒全交给 AQS。

从 lock.lock() 走进 AQS

AQS 到底是什么

AQS 抽象类提供四个共享部件:

  1. 一个 volatile int state。空闲为 0,被占用为 1,重入一次就 +1;Semaphore 把 state 翻译成剩余许可证,CountDownLatch 把它翻译成剩余倒计数。所有同步语义都折算成对这一个字段的 CAS 操作。
  2. 一组基于 Unsafe.compareAndSwapInt 的 CAS,覆盖抢锁、释放、入队、状态翻转。
  3. 一条 FIFO 同步队列(双向链表),抢锁失败的线程挂在这里。
  4. 一对 LockSupport.park / unpark,挂起和恢复线程,避免空转。

ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 都嵌一个继承 AQS 的 Sync 子类,把上面四件套当基建,自己只实现 tryAcquire / tryRelease(独占)或 tryAcquireShared / tryReleaseShared(共享)这几个钩子。骨架是 AQS 的,语义是子类的。

AQS 的四个核心部分

一次抢锁失败的完整过程

线程 A 先到,compareAndSetState(0, 1) 成功,setExclusiveOwnerThread(Thread.currentThread()) 把"我"登记成独占者,tryAcquire 返回 true,acquire 模板结束。

线程 B 接着到。CAS 失败,tryAcquire 返回 false,acquire 模板接管:

线程获取锁失败后的流程

java 复制代码
// AQS 提供的模板方法(简化版,便于理解流程,不是逐行源码)
public final void acquire(int arg) {
    if (!tryAcquire(arg)) {              // 1. 再试一次
        Node node = addWaiter();         // 2. 入队
        acquireQueued(node, arg);        // 3. 在队列里 park
    }
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {               // 1. 释放 state
        Node h = head;
        if (h != null && h.waitStatus != 0) {
            unparkSuccessor(h);          // 2. 唤醒下一个节点
        }
        return true;
    }
    return false;
}

// 子类必须实现的钩子(Mutex 风格简化版:不可重入)
protected boolean tryAcquire(int arg) {
    if (compareAndSetState(0, arg)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

protected boolean tryRelease(int arg) {
    if (getState() == 0 || getExclusiveOwnerThread() != Thread.currentThread()) {
        throw new IllegalMonitorStateException();
    }
    setExclusiveOwnerThread(null);
    // 非重入 Mutex 直接把 state 清成 0;ReentrantLock 这里会做 c-1,
    // 因为重入时 state 是大于 1 的计数,必须逐层释放。
    setState(0);
    return true;
}

AQS 为 B 创建一个 Node 放到队尾,acquireQueued 让 B 在队列里循环:

  • 如果自己是 head 的后继,再 CAS 抢一次------给已经在排队的线程一个窗口,万一 A 刚好释放锁就不用 park。
  • 不是第二个或者再抢又败,把前驱的 waitStatus 改成 SIGNAL("你释放的时候记得叫我")。不设 SIGNAL 的话 park 起来再也没人叫醒。
  • LockSupport.park(this) 挂起当前线程,停在这里不再消耗 CPU。

LockSupport.park 也可能在没有任何 unpark 或 interrupt 的情况下自己返回,即通常说的伪唤醒(spurious wakeup)。防御原则和 Object.wait / Condition.await 完全一致:调用方必须在循环里重新检查谓词(这里是 tryAcquire),不能把 park 返回当成事件已经发生。acquireQueuedfor (;;) 正是这个防御循环------park 一返回就重新 tryAcquire,抢不到就再次 park,既兜底正常唤醒也兜底伪唤醒。

线程 A 干完活调 unlock(),走 release(1)tryRelease 把 state 改回 0,清掉独占者,返回 true。release 拿到 true 之后查的是 head.waitStatus------也就是 head 节点自己的状态字段,不是 head.next。如果它不是 0(通常是 SIGNAL),调 unparkSuccessor(head):先读 head.next;如果 head.next 不为 null 且 waitStatus <= 0,就直接用它;否则从 tail 沿着 prev 反向扫描,保留离 head 最近、且 waitStatus <= 0 的后继节点,再对它做 LockSupport.unpark。这是因为 head.next 可能为 null(队列里只剩 head),或者后继已经被 cancel(waitStatus > 0),此时必须反向兜底找到下一个可唤醒的人。B 在 acquireQueued 里重跑 tryAcquire------这次 CAS 成功,state 变 1,进入临界区。

state 到底代表什么

state 是 volatile int,不同子类把同一个 int 翻译成不同业务:

同步器 state 语义 加减规则
ReentrantLock 重入次数,0 表示空闲;公平性由 tryAcquire 是否先查队列决定 获取时 +1,释放时 -1
Semaphore 剩余许可证数量 acquire 时 -1,release 时 +1
CountDownLatch 剩余计数 countDown 时 -1,到 0 时唤醒所有等待者
ReentrantReadWriteLock 高 16 位为读锁计数,低 16 位为写锁计数 读锁 +1<<16,写锁 +1

看到 getState() 的返回值就能判断锁处于什么状态:Semaphore 的 state = 3 表示还能并发放行 3 个;CountDownLatch 的 state = 0 表示 latch 已打开,所有 await 都已返回。

ReentrantReadWriteLock 的 state 等于"读锁数×65536 + 写锁数(0 或 1)",看一次 getState() 就同时知道读了几把、写了几把。

同步队列怎么工作

同步队列是一条带头结点的双向链表,由 headtail 两个指针维护。头节点是"虚节点",不绑定线程,只是稳定入口,后继才是真正第一个等锁的线程。抢到锁的线程进入临界区之前会调 setHead(node):把 head = node,把 node.threadnode.prev 清成 null;调用方随后再做 p.next = null 把旧前驱节点的 next 指针也断开,让原节点可以被 GC 回收。注意,每次新的抢锁失败仍然会通过 addWaiter 分配一个新的 Node,setHead 不构成对象池------它的作用只是把已经持有锁的节点从队列语义里"摘出去",不再阻塞后面的唤醒路径。

每个 Node 关键字段:

  • thread:当前节点代表的线程;取消后置为 null。
  • waitStatusvolatile int,节点刚插入同步队列时是 0;之后在 0、SIGNAL(-1,后继需要被唤醒)、CANCELLED(1,被取消)、CONDITION(-2,节点在条件队列上)、PROPAGATE(-3,共享模式下传递唤醒)这几个值之间切换,对应节点生命周期的不同阶段。CANCELLED 是终态,被其他线程 park 前顺手清理;其他三个状态是流转中的中间值。
  • prev / next:前后指针,新节点入队时通过 CAS 改 tail
  • nextWaiter:同步队列的顺序仍由 prev / next 维护,nextWaiter 不参与同步队列排序。它只做两件事------把同一张条件队列上的节点串成单链表(Condition.await 用它串条件队列);给当前节点打上模式标记,独占节点对应 Node.EXCLUSIVE(值为 null),共享节点对应 Node.SHARED(值为一个 new Node() 哨兵)。

AQS 同步队列 入队从 tail 加,避免多线程争抢时链表被搞乱;出队从 head 的后继节点开始唤醒。CANCELLED 节点在 shouldParkAfterFailedAcquire 阶段被跳过并清理,清理是惰性的------没有专门清扫线程,是某个线程 park 之前顺手把前面已取消的节点跳掉。

prevnext 都不可少:取消节点时需要把前驱和后继直接连起来跳过自己,只有 next 的话删除要从头遍历,O(n) 不可接受,双向链表把删除降到 O(1)。

park 比自旋节省 CPU:while (true) 空转会吃满一个核心,多线程下自旋也抢不到锁,反而浪费调度。LockSupport.park 底层走 Unsafe.park,最终调用操作系统的阻塞原语(比如 Linux 的 futex),把线程从运行队列移到等待队列,让出 CPU。park 之前 acquireQueued 会再做一次 tryAcquire------这是为了避免多线程竞争时无意义的 park / unpark 往返,比如刚 park 起来就被 unpark,浪费一次上下文切换;自旋没用,但 park 之前再试一次 CAS 是有意义的。

unpark 之后线程要重新进入 acquireQueued 的循环再次 tryAcquire。这个窗口期里另一个线程可能已经把锁抢走------前头插进来一个非公平模式的 ReentrantLock 新线程是常见情况。acquireQueuedfor (;;) 循环就是为此兜底的:每次 park 返回都重新检查谓词并重试,"被唤醒"和"拿到锁"之间永远隔一次重试。

独占模式和共享模式

AQS 把"如何获取同步状态"分成两种语义,对应两个不同的钩子:

  • 独占模式(exclusive) :一次只允许一个线程通过,tryAcquire 返回 boolean。ReentrantLockReentrantReadWriteLock.WriteLock 都是独占,钩子方法是 tryAcquire / tryRelease
  • 共享模式(shared) :一次允许多个线程通过,tryAcquireShared 返回 int:负数代表失败,0 代表成功但后续不再放行,正数代表剩余可用资源。SemaphoreCountDownLatchReentrantReadWriteLock.ReadLock 都是共享,钩子方法是 tryAcquireShared / tryReleaseShared

共享模式 tryAcquireShared 返回 int,必须告诉调用方"我现在还剩多少配额":doAcquireShared 根据返回值判断要不要继续唤醒后面的节点------这就是 PROPAGATE 机制。Semaphore 有 5 个许可证时,tryAcquireShared 在第一个通过时返回 4,第二个返回 3,依此类推;返回正数,AQS 就知道还有余量,把后继一并唤醒让他们一起去抢。如果自定义同步器把 tryAcquireShared 写成返回 boolean,信号传播就丢了,许可证还有剩余但后面的线程一直在 park。

PROPAGATE 这个状态是为共享模式单独设计的,对应 doReleaseShared 的第二条分支。doReleaseShared 在循环里做两件事:

  • 如果 head 当前 waitStatus == SIGNAL:CAS 把 SIGNAL→0,CAS 失败就 retry;成功之后再走 unparkSuccessor(head)
  • 如果 head 当前 waitStatus == 0:CAS 把 0→PROPAGATE,CAS 失败也 retry;这一步本身不 unpark,只是给 head 打一个传播位。

PROPAGATE 的语义是"下一次 release 之前,如果下游 acquireShared 还没来得及看到 release 信号,可以通过 PROPAGATE 把它传播下去"------它处理的是连续 releaseShared 之间 head 状态被快速翻转、或者共享模式的 acquireShared 与 release 交错时的边界情况,保证下游唤醒不丢。它不是"release 时当前没在拿锁就触发"的语义,是 head 状态机的兜底传播位。

ReentrantReadWriteLock 把两种模式叠在一起用:读锁是共享、写锁是独占,AQS 通过 tryAcquireSharedtryAcquire 两套钩子同时支持。一旦写锁被持有,读锁的 tryAcquireShared 返回负数,所有新读者 park;写锁释放后读锁放行,又触发 PROPAGATE 把等待中的读者一次性叫醒一批。

用 AQS 实现 Mutex

不靠 ReentrantLock,直接用 AQS 写一个非重入互斥锁 Mutex,对应 aqs-blog/examples/Mutex.java

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public final class Mutex implements Lock {

    private static final class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int arg) {
            if (arg != 1) {
                throw new IllegalArgumentException("arg must be 1, was: " + arg);
            }
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (arg != 1) {
                throw new IllegalArgumentException("arg must be 1, was: " + arg);
            }
            if (getState() == 0 || getExclusiveOwnerThread() != Thread.currentThread()) {
                throw new IllegalMonitorStateException(
                    "release attempted by non-owner or on an unlocked mutex");
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
        }

        Condition createCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    @Override public void lock()                            { sync.acquire(1); }
    @Override public void unlock()                          { sync.release(1); }
    @Override public boolean tryLock()                      { return sync.tryAcquire(1); }
    @Override public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    @Override public Condition newCondition()               { return sync.createCondition(); }
}

Mutex 只回答一个问题:state 是不是 0。CAS 成功就拿锁,失败就让 AQS 去排队------排队、入队、park、唤醒、重试,全部由 AQS 父类负责。tryRelease 严格校验"是不是当前线程持有",避免别的线程误释放;isHeldExclusively 给 Condition 用,确保只有持有锁的线程才能调 await;newCondition() 返回 AQS 内部提供的 ConditionObjectlockInterruptibly 和带超时的 tryLock(long, TimeUnit) 分别走 acquireInterruptiblytryAcquireNanos,前者响应中断,后者自带超时返回 false------这是 AQS 提供的三套获取语义:阻塞不可中断、阻塞可中断、阻塞限时。

用这个 Mutex 包住一个计数器:

java 复制代码
public final class SafeCounter {

    private final Mutex lock = new Mutex();
    private int value;

    public void increment() {
        lock.lock();
        try {
            value++;
        } finally {
            lock.unlock();
        }
    }

    public int get() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

MutexDemo 用 8 个线程 × 10000 次自增跑一遍:

text 复制代码
expected = 80000
actual = 80000

几十行钩子代码 + 一个 int,就拿到一套和 ReentrantLock 同款的排队机制。Mutex 比 ReentrantLock 简单是因为它不做重入也不分公平/非公平,把这两块补回来就是完整 ReentrantLock。isHeldExclusively 这个钩子在 Condition 路径里被 AQS 调用,用来在 ConditionObject.signal() / signalAll() 之前断言当前线程持有锁;await 路径上的锁所有权不是靠它保证,而是走 fullyRelease(node)release() → 子类的 tryRelease(),非持有者调用 await 会在 tryRelease 那里抛 IllegalMonitorStateException。Mutex 这里只判断 state == 1 && 独占线程 == 当前线程,等效于 ReentrantLock.getHoldCount() > 0 的轻量版。

Condition、公平性和常见误区

Condition 必须从 AbstractQueuedSynchronizer.ConditionObject 创建,本质上是另一条链表------条件队列。它和同步队列共享 state,但每个 ConditionObject 自己维护一张条件队列,互不干扰。

当线程在锁内调用 condition.await() 时:

Condition 节点转移

  1. 当前线程释放锁(相当于走一遍 release),state 回到可用状态。不释放的话别的线程就拿不到锁,永远没人 signal 我们,就会死锁。
  2. AQS 新建一个条件队列上的 NodewaitStatus = CONDITIONthread 指向当前线程),挂到当前 ConditionObject 维护的条件队列尾部。线程之前如果在同步队列里,那个同步队列节点不会被搬过来复用;旧同步节点随后被清理。
  3. 当前线程在条件队列里 park,进入等待状态。

condition.signal() 从条件队列头部摘一个节点,重新挂回同步队列尾部,等它重新排队抢锁(这一步由 ConditionObject 内部的 CAS 完成)。被 signal 唤醒后这个节点会沿着普通 acquireQueued 路径走:先 tryAcquire,抢不到就 park、再被前驱 unpark。和同步队列的新节点相比,它没有"插队"特权------signal 只是把它送回排队起跑线,最终能不能拿到锁还得看队列里其他线程的状态。如果没人 signal,这个线程会一直 park 下去,哪怕锁已经空出来------这也是 await 必须放在循环里判断条件的原因,不能假设 await 一返回条件就成立了。

公平性的选择在 new ReentrantLock(...) 构造期定型:构造时根据 fair 参数决定装哪一套 Sync------new ReentrantLock(true)FairSyncnew ReentrantLock()(默认)选 NonfairSync。两种 Sync 的区别在 tryAcquireFairSync 在调 compareAndSetState 之前先调 hasQueuedPredecessors() 检查同步队列里是否有先到的线程,有就返回 false 让位,没有才允许 CAS 抢锁;NonfairSync 不查队列直接 CAS,允许新线程插队。

公平模式的语义是"严格按入队顺序分配锁":已经在队列里的线程按到达先后排队,新线程必须先看队列里有没有先到的节点,有就让位。但吞吐量会差,每一次传递都要经历 unpark → 唤醒等待线程 → 它重新 tryAcquire → 上下文切换这条完整链路,CPU 和调度成本比非公平模式高。非公平模式则允许新线程不查队列直接抢------吞吐高,但可能出现饥饿。

synchronized 在 JDK 8 里默认也是非公平。HotSpot 内部用 ObjectMonitor 实现,每个 monitor 自带 _cxq(竞争栈,新来的线程压入栈顶)、_EntryList(park 状态的候选队列)、_WaitSetObject.wait 的线程链表)这几个内部结构,但它们的入队策略和 AQS 的双向链表不同------_cxq 是栈、_EntryList 抽线程的顺序也不保证 FIFO,所以即便有队列,行为上也不保证严格意义上的先来后到。在单线程反复进入临界区时会触发偏向锁(biased locking)优化,但只要出现第二个线程争抢,偏向就会撤销,恢复到非公平的轻量级 CAS / 重量级 monitor 路径。ReentrantLock 用 AQS 把这套行为拆成可控的 API------fair 参数、newCondition()tryLock(time)------开发者可以精细控制;synchronized 是 JVM 内置关键字,行为固定、配置项少,但和 AQS 共用同一个 OS 互斥底层(Linux 上都是 futex)。

几个常见误区:

  1. "park 等于 sleep" :唤醒条件差别很大。Thread.sleep 只会在超时之后醒来,或者被 interrupt 中断后通过抛 InterruptedException 醒来;它不响应 unpark,只能靠时间或者中断。LockSupport.park 不抛 InterruptedException------被 unpark 直接返回,被 interrupt 时也是直接返回(标记位留下,调用方自己查 Thread.interrupted())。Thread.sleep 写不出"等一个事件"的语义,LockSupport.park 才是事件等待的工具。

  2. "被唤醒就拿到锁了" :唤醒只是回到 acquireQueued 重新尝试,可能再次失败再次 park。看到 unpark 不要急着下结论"它拿到锁了"。

  3. "AQS 自己就是锁" :AQS 是模板,没有业务语义,必须由子类实现 tryAcquire / tryRelease(独占)或 tryAcquireShared / tryReleaseShared(共享)才有意义。直接 new AbstractQueuedSynchronizer() 没意义,调用 acquire 会因为 tryAcquire 抛 UnsupportedOperationException 而失败。

  4. "中断会打断 park" :默认会,但 acquire 模板里如果 tryAcquire 一直失败,park 被中断后还会再走一遍循环,直到真的拿到锁。普通的 lock() 是不可中断的------调 thread.interrupt() 会记录中断标志位但线程继续 park 等锁;想要可中断地等,得用 lockInterruptibly()。Mutex 上面 lockInterruptibly / tryLock(time, unit) 分别走 sync.acquireInterruptibly / sync.tryAcquireNanosacquireInterruptibly 在入口处先调一次 Thread.interrupted() 检查中断标志位,置位就直接抛 InterruptedException;进队列后由 parkAndCheckInterrupt() 负责------它先 park,park 返回后再查中断标志位,置位就退出循环让上层抛异常。也就是说,中断不是"每次 park 前都查一遍",而是"入口查一次 + 每次 park 返回后查一次"。

读 AQS 源码时容易卡在 shouldParkAfterFailedAcquireparkAndCheckInterrupt 这两个方法上------它们做的事很细,但跳过去只看 acquireQueued 的骨架也能把整条链路跑通。抓住 state、队列、park/unpark 这三件套,剩下的细节都是对它们的补丁。顺带一提,Condition.await 内部也走 park,所以 AQS 的"防御性循环"原则在条件队列里一样适用:signal 之后要重新检查谓词,不能假设条件成立。

相关推荐
神奇小汤圆12 小时前
SpringBoot 相关的 Skills 全景指南
后端
神奇小汤圆13 小时前
高性能进程内队列 BufferQueue 1.0 版本发布
后端
爸爸61913 小时前
空数据状态与引导:多维度 UI 提示设计
后端
Reart14 小时前
Leetcode 674.最长连续递增序列 (719)
后端·算法
芒鸽14 小时前
HarmonyOS ArkUI Search 搜索框:联想词、历史记录与热门标签
后端
Reart14 小时前
Leetcode 714.买卖股票的最佳时机含手续费(719)
后端·算法
芒鸽14 小时前
HarmonyOS ArkUI RichEditor 富文本编辑器:内联样式、Span 管理与格式工具栏
后端
花开彼岸天~14 小时前
Router 路由管理:pushUrl、replaceUrl、back 与参数传递
后端
geovindu15 小时前
CSharp: Decorator Pattern
开发语言·后端·c#·装饰器模式·结构型模式