欢迎光临
我们一直在努力

AQS独占模式源码详解全流程

前言

        在上一篇文章里面只是提到了AQS的大致框架,这篇主要来根据源码来刨析整个拿锁、排队、等待、挂起、唤醒、释放流程;

        AQS:Abstract是抽象,抽象是贯穿整个java的,利用抽象来为上层实现框架,解决了很多问题;AQS是JUC锁底层基石,定义了一个框架来实现并发(利用CAS)

        这个框架主要有三方面特点:① 要有通用性,下层实现透明的同步机制,同时与上层业务解耦 ② 利用CAS原子地修改共享标记位 ③ 如果有的线程必须要获取当前资源那么就必须要有一个等待队列

        那么基于上一篇文章对AQS的了解来进行一下讲解

一、前置基础:Node结点与waitStatus关键状态

// waitStatus四种核心常量(独占只关注SIGNAL、CANCELLED)
static final int CANCELLED = 1; // 节点取消,线程放弃抢锁
static final int SIGNAL = -1; // 后继节点需要前驱唤醒(最核心)
static final int CONDITION = -2; // Condition条件队列使用
static final int PROPAGATE = -3; // 共享锁用

  • Node.prev/next:双向链表组成同步队列;thread:绑定排队线程;nextWaiter=EXCLUSIVE标记独占节点。
  • state:int 同步状态,独占锁:state=0无锁,state>0持有锁(支持重入)。

二、独占锁获取完整过程:acquire入口

先来大致了解一下相关源码:

  • acquire(int arg):AQS 顶层入口,独占式获取锁(不可中断)
  • acquireQueued(Node node,int arg):入队后自旋阻塞抢锁核心,interrupted 变量在这里定义
  • parkAndCheckInterrupt():阻塞线程 + 返回本次 park 期间是否被中断
  • selfInterrupt():线程补中断标记

acquire顶层源码

public final void acquire(int arg) {
// ①快速抢锁失败 && 入队自旋阻塞成功 → 等待期被中断,补中断
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

执行三段拆分:

  tryAcquire快速抢锁 → addWaiter封装节点入队 → acquireQueued自旋阻塞排队。

(1) 阶段 1:tryAcquire (arg) 快速路径:非排队直接抢锁(子类实现)

AQS 空实现抛异常,由 ReentrantLock 公平 / 非公平 Sync 重写,成功返回 true 直接结束,失败进入入队逻辑。

非公平锁 tryAcquire 核心逻辑(NonfairSync)
  • state==0:CAS 把 state 设为 arg (1),设置exclusiveOwnerThread=当前线程,抢锁成功;
  • state≠0 && 当前线程==持有锁线程:state+=arg,实现锁重入,返回 true;
  • 其他场景:抢锁失败返回 false,执行addWaiter入队。
  • 公平锁额外增加:同步队列有排队节点则直接返回 false,禁止插队,保证 FIFO。

    结论:tryAcquire 是乐观快速路径,能拿到锁就不走队列,优化性能。

    (2) 阶段 2:addWaiter (Node.EXCLUSIVE):抢锁失败→封装线程为 Node,插入同步队列尾部

    private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 队列已初始化:CAS快速入队尾
    if (pred != null) {
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
    pred.next = node;
    return node;
    }
    }
    // 队列未初始化/快速CAS失败:enq自旋CAS初始化队列+入队
    enq(node);
    return node;
    }

    enq 队列初始化逻辑:
  • head==null:CAS 新建空哨兵 head 节点(哨兵节点不绑定任何线程,仅占位),head=tail;
  • 再次循环:把当前 node 挂在 tail 后,CAS 更新 tail,完成入队;
  • 同步队列初始:head(空哨兵) ↔ 排队节点1 ↔ 排队节点2 ↔ tail,只有 head 后继才有资格抢锁(FIFO)。

    (3) 阶段 3:acquireQueued (final Node node, int arg):入队后自旋、阻塞、interrupted 变量核心逻辑(重点)

    final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; // 标记是否异常取消节点(finally用)
    try {
    boolean interrupted = false; // 【核心变量】暂存阻塞期间中断状态
    for (;;) { // 死循环自旋
    final Node p = node.predecessor(); // 获取前驱节点
    // 分支1:前驱是head哨兵 → 当前是队列首位,尝试抢锁
    if (p == head && tryAcquire(arg)) {
    setHead(node); // 当前节点晋升新head,原head出队
    p.next = null; // 原head断开引用,帮助GC
    failed = false;
    return interrupted; // 抢到锁,返回中断标记
    }
    // 分支2:抢锁失败 → 校验前驱状态、决定是否park挂起
    if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
    interrupted = true; // 被中断唤醒,仅标记,不跳出循环
    }
    } finally {
    if (failed) // 代码异常跳出循环,取消节点
    cancelAcquire(node);
    }
    }

    代码详解:

    ① 这里的interrupted变量是非常重要的:

    • 变量作用:记录线程在LockSupport.park()阻塞休眠阶段是否收到过中断信号;
    • 关键细节:parkAndCheckInterrupt()内部Thread.interrupted():返回中断标记 + 清空线程原生中断位;
      • 若阻塞被中断唤醒 → parkAndCheckInterrupt()=true → interrupted=true;
      • 正常被前驱 unpark 唤醒 → false,interrupted 不变;
    • AQS 不可中断语义:标记中断但不终止排队: 哪怕中途被 interrupt,仅记录interrupted=true,继续自旋抢锁;只有成功拿到锁后,才把标记返回上层 acquire。
    • 上层acquire拿到 true:执行selfInterrupt(),手动Thread.currentThread().interrupt(),补回被清空的中断位,上层业务代码可感知中断(AQS 不吞中断,延后处理)。

    ② shouldParkAfterFailedAcquire (p,node):挂起前置校验,修改前驱 waitStatus=SIGNAL (-1)

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
    // 前驱已标记SIGNAL:前驱释放锁会唤醒自己,可以安心park
    return true;
    if (ws > 0) {
    // 前驱CANCELLED(1):向前遍历,剔除所有取消节点,重新绑定前驱
    do {
    node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
    } else {
    // ws=0:前驱无状态,CAS改成SIGNAL,告知前驱:你释放务必唤醒我
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false; // 本轮不park,下一轮循环再判断
    }

            SIGNAL 是 “预约唤醒”:当前节点要睡觉前,必须让前驱打上 SIGNAL,保证释放锁时一定会唤醒后继;第一次 CAS 改状态返回 false 不阻塞,第二次循环 ws=SIGNAL 返回 true 进入 park。

    ③ parkAndCheckInterrupt ():真正挂起线程,等待唤醒

    private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 线程进入WAITING阻塞,两种唤醒:unpark/中断
    return Thread.interrupted();
    }

    • 正常唤醒:前驱释放锁unparkSuccessor→park 结束,返回 false;
    • 中断唤醒:其他线程调用thread.interrupt()唤醒,返回 true,interrupted 置 true 继续自旋抢锁。

    ④ 自旋两种结束场景

    • 正常:轮到自己(前驱 = head)+tryAcquire 成功→晋升 head,返回 interrupted;
    • 异常:try 代码抛异常→failed 保持 true→finally 执行cancelAcquire取消排队节点。

    (4)阶段4:selfInterrupt ():抢到锁后补中断

    static void selfInterrupt() {
    Thread.currentThread().interrupt();
    }

    • 若acquireQueued返回true:说明阻塞等待期间被中断,但线程已经成功抢到锁;
    • Thread.interrupted()在 park 时已经清空了中断标记,因此需要selfInterrupt()重新设置中断位,把之前被吞掉的中断补上;
    • 上层业务代码后续调用Thread.isInterrupted()就能感知到中断。

    AQS独占加锁的流程图如下:

    三、独占锁释放完整流程

    release顶层源码(释放锁统一入口)

    public final boolean release(int arg) {
    // ①子类实现tryRelease:锁完全释放返回true,部分重入释放返回false
    if (tryRelease(arg)) {
    Node h = head;
    // head非空&&waitStatus≠0(有等待唤醒的后继)→唤醒head后继
    if (h != null && h.waitStatus != 0)
    unparkSuccessor(h);
    return true;
    }
    return false;
    }

    (1)阶段1:tryRelease (arg):子类实现,释放同步状态(ReentrantLock Sync)

    protected final boolean tryRelease(int releases) {
    int c = getState() – releases;
    // 非法释放:非持有锁线程抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
    throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { // state归零,锁完全释放
    free = true;
    setExclusiveOwnerThread(null);
    }
    setState(c); // 无并发竞争,无需CAS
    return free;
    }

    • 重入场景:连续 lock 两次,state=2,第一次 unlock→c=1≠0→free=false,不唤醒队列;
    • 完全释放:c=0→free=true,进入unparkSuccessor唤醒 head 后继排队线程。

    (2)阶段 2:unparkSuccessor (Node head):唤醒 head 后面第一个有效等待节点

    private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 把head状态从SIGNAL改成0
    if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    // 后继取消/为空:从tail倒着找离head最近的非CANCELLED节点
    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); // 唤醒目标线程
    }

    这里为什么不直接看head后面的节点,而是倒序查找?

            节点入队是先改 prev 再 CAS 改 tail,next 指针并发下可能断裂,prev 指针稳定,从后往前找避免漏找有效节点;唤醒后被阻塞线程从park()处恢复,回到 acquireQueued 死循环继续抢锁。

    四、全流程

            这里将整个过程用一个例子来帮大家顺一遍,更加清晰:T0持有锁、T1/T2排队

  • T0 执行 lock ():tryAcquire(1)CAS 修改 state=1,owner=T0,直接拿到锁,无入队;
  • T1 执行 lock ():tryAcquire 失败→addWaiter 创建 EXCLUSIVE 节点,队列初始化 head (空)→T1 节点入队 tail→acquireQueued 自旋;
    • T1 前驱 = head≠head(初次)→shouldPark 改 head.ws=SIGNAL (-1),本轮不 park;
    • 第二轮循环 ws=SIGNAL→parkAndCheckInterrupt,T1 进入 WAITING 阻塞;interrupted=false;
  • T2 执行 lock ():tryAcquire 失败→入队 tail,前驱 = T1→shouldPark 把 T1.ws 改成 SIGNAL,T2 后续 park 阻塞; 队列:head (-1) ↔ T1 (-1) ↔ T2 (0)(最终 T2 被挂起);
  • T0 调用 unlock ()→release (1):tryRelease→state=0、owner=null,free=true;head≠null&&ws=-1→unparkSuccessor 唤醒 T1;
  • T1 被 unpark 唤醒:跳出 park,回到 acquireQueued 循环,p=head→tryAcquire 成功,state=1,T1 晋升新 head,原 head.next=null 被 GC;返回 interrupted=false→上层 acquire 不执行 selfInterrupt;
  • T1 后续 unlock:同理唤醒 T2,T2 开始抢锁。
  • 赞(0)
    未经允许不得转载:171主机测评 » AQS独占模式源码详解全流程
    分享到: 更多 (0)

    评论 抢沙发

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