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

前言

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

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

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

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

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

java 复制代码
// 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顶层源码

java 复制代码
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)
  1. state==0:CAS 把 state 设为 arg (1),设置exclusiveOwnerThread=当前线程,抢锁成功;
  2. state≠0 && 当前线程==持有锁线程state+=arg,实现锁重入,返回 true;
  3. 其他场景:抢锁失败返回 false,执行addWaiter入队。

公平锁额外增加:同步队列有排队节点则直接返回 false,禁止插队,保证 FIFO。

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

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

java 复制代码
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 队列初始化逻辑:
  1. head==null:CAS 新建空哨兵 head 节点(哨兵节点不绑定任何线程,仅占位),head=tail;
  2. 再次循环:把当前 node 挂在 tail 后,CAS 更新 tail,完成入队;

同步队列初始:head(空哨兵) ↔ 排队节点1 ↔ 排队节点2 ↔ tail只有 head 后继才有资格抢锁(FIFO)

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

java 复制代码
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)

java 复制代码
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 ():真正挂起线程,等待唤醒

java 复制代码
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 ():抢到锁后补中断

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

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

三、独占锁释放完整流程

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

java 复制代码
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)

java 复制代码
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 后面第一个有效等待节点

java 复制代码
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排队

  1. T0 执行 lock ()tryAcquire(1)CAS 修改 state=1,owner=T0,直接拿到锁,无入队;
  2. 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;
  3. T2 执行 lock ():tryAcquire 失败→入队 tail,前驱 = T1→shouldPark 把 T1.ws 改成 SIGNAL,T2 后续 park 阻塞; 队列:head (-1) ↔ T1 (-1) ↔ T2 (0)(最终 T2 被挂起);
  4. T0 调用 unlock ()→release (1):tryRelease→state=0、owner=null,free=true;head≠null&&ws=-1→unparkSuccessor 唤醒 T1;
  5. T1 被 unpark 唤醒:跳出 park,回到 acquireQueued 循环,p=head→tryAcquire 成功,state=1,T1 晋升新 head,原 head.next=null 被 GC;返回 interrupted=false→上层 acquire 不执行 selfInterrupt;
  6. T1 后续 unlock:同理唤醒 T2,T2 开始抢锁。
相关推荐
逻极6 天前
Java 从入门到精通:核心原理、最佳实践与性能优化
java·jvm·并发编程·集合框架
better_liang7 天前
每日Java面试场景题知识点之-JUC并发编程核心原理与实战
java·线程池·并发编程·juc·aqs·reentrantlock·concurrenthashmap
tongluowan00710 天前
以ReentrantLock为例解释AQS的工作流程
java·模板方法模式·aqs·reentrantlock
Tsuki_tl14 天前
【总结】Java的线程状态
java·后端·面试·多线程·并发编程·线程状态
长谷深风11116 天前
多线程并发实战:从原理到应用【个人八股】
java·并发编程·线程安全·java多线程·synchronized·锁升级
萧曵 丶20 天前
JUC 实际业务高频面试题浅谈
java·juc·aqs·lock
Thanks_ks23 天前
分布式锁:Redis 与 Redisson 的工程实践与避坑指南
java·redis·分布式锁·redisson·微服务架构·并发编程·高可用
Chen--Xing24 天前
Python -- 并发编程
python·多线程·并发编程
青山师25 天前
CompletableFuture深度解析:异步编程范式与源码实现
java·单例模式·面试·性能优化·并发编程