Java 并发锁体系全解:从 synchronized、Lock 到 AQS 底层原理与源码深度剖析

在 Java 并发编程体系中,锁是解决多线程共享资源竞争、保证线程安全的核心机制。从 JVM 内置的synchronized隐式锁,到 JUC 包提供的Lock显式锁,再到支撑整个并发工具集的底层基石 AbstractQueuedSynchronizer(AQS),构成了一套层层递进、设计精妙的锁实现体系。

本文将从上层 API 到底层源码,完整拆解 Java 锁的核心原理,覆盖独占模式与共享模式、公平锁与非公平锁、条件队列与双队列联动、读写锁实现等核心知识点,结合 JDK 源码逐层剖析其设计思想与执行流程。

一、Java 锁的两类核心实现:synchronized 与 Lock

1.1 synchronized:JVM 内置隐式锁

synchronized是 JVM 层面实现的内置互斥锁,核心特性是隐式获取与释放锁 ,无需开发者手动干预:

  • 进入同步代码块 / 同步方法时,JVM 自动为当前线程加锁;代码正常执行完成或抛出异常时,自动释放锁
  • 底层依托对象头的 Mark Word 实现,内置偏向锁、轻量级锁、重量级锁的自适应升级机制
  • 局限性突出:不支持响应中断、不支持超时获取、仅能绑定一个等待条件,无法适配复杂的并发协作场景

1.2 Lock:API 层显式锁的灵活控制

Lockjava.util.concurrent.locks包下的顶层接口,属于 API 层面的显式锁,需要手动调用方法完成锁的获取与释放,相比synchronized具备更强的可操作性与场景适配能力:

  • 支持可中断获取锁:等待过程中可以响应线程中断,终止等待
  • 支持超时获取锁:在指定时长内尝试获取锁,超时自动放弃,避免永久阻塞
  • 支持绑定多个条件变量,可实现更精细的等待 / 通知逻辑
  • 支持公平锁与非公平锁模式切换,可根据业务场景选择调度策略

Lock 接口的核心方法如下:

  • lock():阻塞式获取锁,获取成功前持续等待,不响应中断
  • unlock():释放锁,必须手动调用,通常放在finally块中确保执行
  • tryLock():非阻塞尝试获取锁,立即返回布尔结果,不阻塞线程
  • lockInterruptibly():可中断式获取锁,等待过程中响应中断并抛出异常
  • newCondition():创建绑定当前锁的Condition条件变量,一个锁可绑定多个独立条件

JUC 中 Lock 的核心实现类ReentrantLock,以及读写锁、信号量等工具,其底层全部基于 AQS 框架实现。

二、AQS:并发同步器的核心基石

AbstractQueuedSynchronizer(简称 AQS)是 Java 并发包中绝大多数同步工具(ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等)的底层实现框架,它定义了一套多线程访问共享资源的通用同步机制,通过模板方法模式将通用逻辑封装,子类仅需实现少量业务方法即可定制同步器。

2.1 核心组件 1:volatile 修饰的同步状态 state

AQS 通过一个被volatile修饰的整型变量state表示同步状态,这是整个同步器的核心状态变量:

arduino 复制代码
private volatile int state;
  • 独占模式下:通常state=0代表锁未被占用,state=1代表锁已被持有,可重入场景下 state 随重入次数递增
  • 共享模式下:state 代表可用的共享资源总数

volatile保证了 state 在多线程间的可见性 ,AQS 同时提供compareAndSetState(int expect, int update)方法,基于 CAS 操作保证 state 修改的原子性,避免多线程并发修改的线程安全问题。

2.2 核心组件 2:FIFO 双向同步队列

当线程获取同步状态失败时,AQS 会将线程封装为Node节点,加入一个FIFO(先进先出)的双向链表同步队列,通过队列实现线程的等待调度与唤醒管理。

队列的节点为 AQS 的内部类Node,核心字段如下:

arduino 复制代码
static final class Node {
    // 节点等待状态
    volatile int waitStatus;
    // 前驱节点指针
    volatile Node prev;
    // 后继节点指针
    volatile Node next;
    // 节点绑定的等待线程
    volatile Thread thread;
    // 条件队列后继节点,区分独占/共享模式
    Node nextWaiter;
}
  • 队列结构:双向链表,包含头节点head和尾节点tail,头节点代表当前持有同步状态的节点,后续节点按入队顺序等待
  • 节点模式:分为独占模式Node.EXCLUSIVE和共享模式Node.SHARED,通过nextWaiter字段区分

三、AQS 独占式同步状态的获取与释放

独占模式的核心特性是:同一时间只能有一个线程持有同步状态,对应 ReentrantLock 等互斥锁的实现。

3.1 独占式获取:acquire () 源码全解析

acquire(int arg)是独占模式下获取同步状态的入口方法,也是lock()方法的底层核心:

scss 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

执行流程分为四步:

  1. 尝试快速获取 :调用tryAcquire(arg)尝试直接获取同步状态,成功则方法直接返回
  2. 节点封装入队 :获取失败时,调用addWaiter将当前线程封装为独占模式节点,加入同步队列尾部
  3. 队列自旋等待 :调用acquireQueued,让节点在队列中自旋等待,直到获取到同步状态
  4. 中断标记补全 :若等待过程中线程被中断过,最后调用selfInterrupt()补上中断标记

分步核心源码解析

  1. tryAcquire:子类实现的获取逻辑 AQS 本身不实现具体的获取逻辑,交由子类根据业务特性重写,是模板方法模式的核心体现:

    protected boolean tryAcquire(int arg) {

    throw new UnsupportedOperationException();

    }

    例如 ReentrantLock 的公平锁与非公平锁,就在该方法中实现了不同的抢占规则。

  2. addWaiter:节点加入同步队列 将当前线程封装为 Node 节点,先通过 CAS 快速尝试加入队尾,失败则进入自旋入队逻辑:

    private Node addWaiter(Node mode) {

    Node node = new Node(Thread.currentThread(), mode);

    // 快速尝试:CAS直接设置队尾

    Node pred = tail;

    if (pred != null) {

    node.prev = pred;

    if (compareAndSetTail(pred, node)) {

    pred.next = node;

    return node;

    }

    }

    // 快速尝试失败,进入自旋+CAS的enq方法

    enq(node);

    return node;

    }

    3.enq:自旋保证节点安全入队 通过死循环 + CAS,保证高并发下节点一定能成功入队,同时完成队列的懒初始化:

    private Node enq(final Node node) {

    for (;;) {

    Node t = tail;

    // 队列为空,初始化哨兵头节点

    if (t == null) {

    if (compareAndSetHead(new Node()))

    tail = head;

    } else {

    node.prev = t;

    if (compareAndSetTail(t, node)) {

    t.next = node;

    return t;

    }

    }

    }

    }

    4.acquireQueued:队列中自旋获取锁 节点入队后进入自旋逻辑:只有前驱节点是头节点时,才尝试获取同步状态;否则调整前驱状态后阻塞等待:

    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; // 帮助GC回收旧头节点

    failed = false;

    return interrupted;

    }

    // 获取失败,判断是否需要阻塞当前线程

    if (shouldParkAfterFailedAcquire(p, node) &&

    parkAndCheckInterrupt())

    interrupted = true;

    }

    } finally {

    if (failed)

    cancelAcquire(node);

    }

    }

    3.2 waitStatus 节点等待状态详解

    Node 节点的waitStatus字段控制着线程的等待状态与唤醒逻辑,JDK 1.8 中共有 5 种取值:

状态值 常量名 含义说明
0 - 初始状态,节点刚创建入队时的默认状态
-1 SIGNAL 当前节点的后继节点处于阻塞状态,当前节点释放同步状态或取消时,必须唤醒后继节点
1 CANCELLED 线程因超时、中断等原因取消等待,节点作废,不再参与同步竞争
-2 CONDITION 节点位于 Condition 条件队列中,等待条件满足,此时不在同步队列内
-3 PROPAGATE 共享模式下,唤醒操作需要向后传播,确保所有等待的共享节点都能被通知

其中shouldParkAfterFailedAcquire方法是状态流转的核心,负责调整前驱节点状态并判断当前线程是否可以安全阻塞:

arduino 复制代码
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 前驱已设为SIGNAL,当前线程可以安全阻塞
    if (ws == Node.SIGNAL)
        return true;
    // 前驱节点已取消,向前跳过所有取消节点,重新链接队列
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 前驱为初始状态或PROPAGATE,CAS修改为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

3.3 正常流程与取消流程的状态流转

正常流程下的状态流转

在无取消、无异常的正常竞争场景下,节点的状态流转如下:

  1. 节点入队:新创建的节点 waitStatus 为初始值 0,加入队列尾部
  2. 状态修改 :节点执行shouldParkAfterFailedAcquire,将前驱节点的 waitStatus 从 0 CAS 修改为 SIGNAL (-1)
  3. 线程阻塞 :下一轮自旋再次尝试获取失败后,确认前驱状态为 SIGNAL,调用LockSupport.park()阻塞当前线程
  4. 被唤醒:头节点释放同步状态时,唤醒后继节点,当前线程从 park 中苏醒
  5. 获取成功:线程再次尝试获取同步状态,成功后将自己设为新的头节点,旧头节点出队,完成一次状态流转

取消流程下的状态流转

当线程等待过程中发生异常、中断或超时,会触发节点取消流程,核心逻辑在cancelAcquire方法中:

ini 复制代码
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    node.thread = null;

    // 向前遍历,跳过所有已取消的前驱节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    Node predNext = pred.next;

    // 将当前节点状态设为取消
    node.waitStatus = Node.CANCELLED;

    // 情况1:当前节点是尾节点,更新队尾指针
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        // 情况2:当前节点不是头节点的后继,将前驱与后继节点链接
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 情况3:当前节点是头节点的后继,直接唤醒后继节点
            unparkSuccessor(node);
        }
        node.next = node; // 帮助GC
    }
}

取消流程的核心是:将节点标记为 CANCELLED,调整队列双向指针,把取消节点从同步队列中剔除,保证队列的有效性。

3.4 独占式同步状态释放

release(int arg)是独占模式下释放同步状态的入口方法:

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. 调用tryRelease(arg)尝试释放同步状态,由子类实现具体逻辑,释放成功返回 true
  2. 释放成功后,检查头节点状态:若头节点存在且 waitStatus 不为 0,调用unparkSuccessor唤醒后继等待线程

唤醒后继节点的unparkSuccessor方法:

ini 复制代码
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) {
        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);
}

为什么从队尾往前找后继节点?因为节点入队时先设置 prev 指针、再 CAS 更新 tail、最后设置前驱的 next 指针,从后往前遍历能保证不会漏掉刚入队的节点,避免空指针问题。

四、ReentrantLock:公平锁与非公平锁源码对比

ReentrantLock 是 AQS 独占模式最经典的实现,也是可重入互斥锁的标准实现。它通过内部两个 AQS 子类,实现了公平锁(FairSync)非公平锁(NonfairSync) 两种模式,二者的核心差异完全体现在对tryAcquire方法的不同实现上。

4.1 ReentrantLock 整体类结构

ReentrantLock 内部维护了一个继承自 AQS 的抽象内部类Sync,作为公共逻辑基类;再由FairSyncNonfairSync两个子类分别实现公平与非公平策略。

scala 复制代码
public class ReentrantLock implements Lock, java.io.Serializable {
    // 同步器基类,继承AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
        // 公共非公平获取逻辑,供非公平锁直接调用
        final boolean nonfairTryAcquire(int acquires) { ... }
    }

    // 非公平锁实现
    static final class NonfairSync extends Sync { ... }

    // 公平锁实现
    static final class FairSync extends Sync { ... }

    // 默认构造:非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    // 传入true指定公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

可重入特性:两种锁都支持重入,即同一个线程可以多次获取同一把锁,state值随重入次数递增,释放时对应递减,直到 state 归 0 才算完全释放锁。重入逻辑在两种模式下完全一致。

4.2 非公平锁 NonfairSync 源码解析

非公平锁是ReentrantLock的默认实现,核心特点是:线程获取锁时不考虑队列中是否有等待线程,直接尝试抢占,抢占失败才进入队列排队

lock () 入口:上来就插队

非公平锁的lock()方法,在进入 AQS 的acquire流程之前,会先通过 CAS 直接尝试抢一次锁,这是第一次插队机会。

scss 复制代码
final void lock() {
    // 第一次插队:直接CAS尝试把state从0改成1
    if (compareAndSetState(0, 1))
        // 抢锁成功,设置当前线程为锁的持有者
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 抢占失败,走AQS标准的acquire获取流程
        acquire(1);
}

tryAcquire:再次插队,不判断队列

非公平锁的tryAcquire直接调用基类的nonfairTryAcquire,核心逻辑是:只要锁空闲(state=0),不管同步队列里有没有等待了很久的线程,直接 CAS 抢锁,这是第二次插队机会。

java 复制代码
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

// Sync基类中的公共非公平获取逻辑
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 锁处于空闲状态
    if (c == 0) {
        // 直接CAS抢锁,不判断队列是否有等待线程
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 锁已被占用,判断是不是当前线程持有(重入逻辑)
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // 重入次数溢出
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 抢锁失败
    return false;
}

4.3 公平锁 FairSync 源码解析

公平锁的核心原则是先来先服务(FIFO) :线程获取锁时,必须先检查同步队列中是否有其他线程在等待,只有队列中没有更早的等待线程时,才尝试获取锁,否则直接进入队列排队。

lock () 入口:不插队,直接走标准流

公平锁的lock()方法没有提前抢锁的操作,直接调用 AQS 的acquire方法,严格遵守排队规则。

scss 复制代码
final void lock() {
    acquire(1);
}

tryAcquire:公平性核心,先判断队列

公平锁的tryAcquire与非公平锁的唯一区别,就是在锁空闲时,会先调用hasQueuedPredecessors()判断队列中是否有前驱等待线程,只有没有更早的等待者时,才尝试获取锁。

java 复制代码
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;
        }
    }
    // 重入逻辑与非公平锁完全一致
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平性核心:hasQueuedPredecessors

这是实现公平锁的关键方法,用于判断同步队列中是否存在比当前线程等待更久的线程

ini 复制代码
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // 返回true:队列中有更早的等待线程,当前线程不能插队
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

逻辑拆解:

  1. h != t:头节点不等于尾节点,说明队列不为空,有线程在等待
  2. (s = h.next) == null:头节点的后继节点为空,说明有线程正在执行入队操作(已经设置了 tail,但还没设置 prev 的 next 指针),此时认为队列中有等待者
  3. s.thread != Thread.currentThread():后继节点存在,但绑定的线程不是当前线程,说明有其他线程比当前线程等待更久

只要满足 "队列不为空,且第一个等待线程不是当前线程",就返回 true,当前线程必须排队,从而保证公平性。

4.4 公平锁 vs 非公平锁 核心对比

对比维度 非公平锁(默认) 公平锁
插队机会 两次插队:lock 时直接 CAS 抢锁;tryAcquire 时再次抢锁 无任何插队机会,严格遵循 FIFO
核心判断 锁空闲就直接抢,不关心等待队列 锁空闲时必须先判断队列,无更早等待者才抢
吞吐量 高,线程挂起唤醒的上下文切换次数少 低,所有线程严格排队,上下文切换频繁
线程饥饿 可能出现:新来的线程一直插队,队列中的线程长期获取不到锁 不会出现,所有线程按等待顺序依次获取
适用场景 绝大多数业务场景,追求高吞吐量、高性能 对执行顺序有严格要求,需要避免饥饿的场景

五、Condition 条件队列:双队列联动与等待唤醒机制

Condition 是显式锁体系对 "等待 - 通知" 模式的进阶实现。它解决了内置锁 Object.wait()/notify() 只能绑定一个等待队列、无法实现精准唤醒的缺陷,一个 Lock 可以绑定多个独立的 Condition 条件队列,分别对应不同的等待条件,典型应用如 ArrayBlockingQueue 的 "队列非空""队列非满" 双条件设计。

Condition 本身是接口,其核心实现是 AQS 的内部类 ConditionObject,它依托 AQS 的同步队列,额外维护了一套独立的条件等待队列,节点在 "条件队列" 与 "同步队列" 之间完成状态流转,实现等待与唤醒的完整闭环。

5.1 条件队列的结构设计

核心字段与链表结构

ConditionObject 内部维护了一个**单向链表**结构的条件等待队列,通过头尾两个指针定位队列,节点复用 AQS 的 Node 内部类,通过 nextWaiter 字段串联链表。

java 复制代码
public class ConditionObject implements Condition, java.io.Serializable {
    // 条件队列头节点:第一个等待条件的节点
    private transient Node firstWaiter;
    // 条件队列尾节点:最后一个等待条件的节点
    private transient Node lastWaiter;

    // 中断处理模式:等待结束后抛出中断异常
    private static final int THROW_IE    = -1;
    // 中断处理模式:等待结束后重置中断标记
    private static final int REINTERRUPT =  1;
}

同步队列与条件队列的核心差异

条件队列中的节点,waitStatus 固定为 Node.CONDITION(-2),代表节点正在等待条件触发,暂时脱离锁的竞争。

维度 同步队列(AQS 内置) 条件队列(Condition 维护)
链表结构 双向链表,prev/next 指针 单向链表,nextWaiter 指针
等待目标 等待获取锁(同步状态) 等待某个业务条件满足
节点状态 0、SIGNAL(-1)、CANCELLED(1)、PROPAGATE(-3) CONDITION(-2)
数量 1 个 Lock 对应 1 个同步队列 1 个 Lock 可对应 N 个条件队列
节点归属 未抢到锁的线程全部在此排队 主动调用 await () 的线程在此等待

一个节点同一时间只能存在于一个队列中:要么在同步队列抢锁,要么在条件队列等条件,唤醒过程本质就是节点从条件队列迁移到同步队列的过程。

5.2 await () 等待流程源码解析

调用 await() 的前置条件:当前线程必须已经持有对应 Lock 锁 ,这和 Object.wait() 必须在同步块中调用是同一逻辑 ------ 保证队列修改与锁释放的线程安全性

await() 的完整执行逻辑可以概括为五步:

  1. 线程安全地创建节点,加入条件队列尾部
  2. 完全释放当前持有的锁(处理可重入场景)
  3. 阻塞当前线程,等待被唤醒或中断
  4. 被唤醒后,从条件队列迁移至同步队列
  5. 在同步队列中重新竞争锁,竞争成功后恢复执行

await () 入口主流程

scss 复制代码
public final void await() throws InterruptedException {
    // 1. 前置校验:线程已中断则直接抛异常
    if (Thread.interrupted())
        throw new InterruptedException();
    
    // 2. 创建CONDITION状态节点,加入条件队列尾部
    Node node = addConditionWaiter();
    
    // 3. 完全释放锁(含重入次数),返回释放前的state值
    int savedState = fullyRelease(node);
    
    int interruptMode = 0;
    // 4. 自旋:只要节点还没进入同步队列,就持续阻塞
    while (!isOnSyncQueue(node)) {
        // 阻塞当前线程
        LockSupport.park(this);
        // 检查是否因中断被唤醒,记录中断模式
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // 5. 节点已进入同步队列,调用acquireQueued重新抢锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    
    // 6. 清理条件队列中已取消的节点
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    
    // 7. 根据中断模式处理最终结果
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

核心子方法解析

  1. addConditionWaiter:加入条件队列 创建状态为 CONDITION 的节点,追加到条件队列尾部;如果发现尾节点已取消,先执行一次队列清理。

    private Node addConditionWaiter() {

    Node t = lastWaiter;

    // 尾节点已取消,先清理所有无效节点

    if (t != null && t.waitStatus != Node.CONDITION) {

    unlinkCancelledWaiters();

    t = lastWaiter;

    }

    // 新建节点,状态为CONDITION

    Node node = new Node(Thread.currentThread(), Node.CONDITION);

    // 队列为空则设为头节点,否则追加到尾部

    if (t == null)

    firstWaiter = node;

    else

    t.nextWaiter = node;

    lastWaiter = node;

    return node;

    }

2.fullyRelease:完全释放锁

因为 ReentrantLock 是可重入锁,线程可能多次获取锁(state>1),调用 await 时必须一次性释放全部同步状态,否则其他线程永远无法获取锁;同时记录释放前的 state 值,后续重新获锁时恢复重入次数。

ini 复制代码
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        // 一次性释放全部state
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            // 释放失败说明当前线程未持有锁,抛出非法监视器异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        // 释放失败则标记节点为取消状态
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

5.3 signal () 唤醒流程源码解析

signal()的核心作用并不是直接唤醒线程让它运行,而是把满足条件的节点从条件队列,迁移到同步队列尾部,线程并不会立刻执行,它需要进入同步队列后,排队等待获取锁,才能真正恢复运行。

signal () 主流程

java 复制代码
public final void signal() {
    // 前置校验:当前线程必须持有锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        // 唤醒条件队列中第一个有效节点
        doSignal(first);
}

doSignal:遍历唤醒首个有效节点

从条件队列头开始遍历,找到第一个未取消的节点,执行转移操作;如果节点已取消则继续往后找。

typescript 复制代码
private void doSignal(Node first) {
    do {
        // 头节点后移,若后续无节点则尾指针置空
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // 转移节点到同步队列,失败则继续处理下一个
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

transferForSignal:节点转移核心

这是双队列联动的核心方法,完成节点从条件队列到同步队列的迁移与状态转换:

arduino 复制代码
final boolean transferForSignal(Node node) {
    // 1. CAS修改状态:从CONDITION改为初始0
    // 修改失败说明节点已被取消,直接返回false
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 2. 调用enq方法,将节点加入同步队列尾部,返回前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    
    // 3. 检查前驱节点状态:
    // - 前驱已取消,或无法设置为SIGNAL,则直接唤醒当前线程
    // - 让线程自己去同步队列中竞争并清理无效节点
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    
    return true;
}

这里有一个关键设计:signal () 不一定会立刻 unpark 线程。如果前驱节点状态正常且成功设为 SIGNAL,就不唤醒线程,等前驱节点释放锁时再统一唤醒,减少不必要的上下文切换。

5.4 双队列完整状态流转

结合 AQS 同步队列,节点的全生命周期流转对应线程的等待与唤醒全过程:

  1. 持有锁阶段:线程 A 成功获取锁,成为同步队列头节点对应的执行线程,state>0。
  2. 进入条件队列 :线程 A 调用 await(),创建 waitStatus=CONDITION 的节点,追加到条件队列尾部;同时调用 fullyRelease 完全释放锁,state 归零。
  3. 阻塞等待 :线程 A 执行 LockSupport.park() 进入阻塞状态,此时节点仅存在于条件队列,脱离同步队列。
  4. 触发唤醒 :线程 B 获取锁后调用 signal(),取出条件队列头节点,CAS 将 waitStatus 从 CONDITION 改为 0,调用 enq 将节点加入同步队列尾部。
  5. 同步队列排队:节点进入同步队列后,遵循独占锁的排队规则:前驱节点设为 SIGNAL,线程继续阻塞等待。
  6. 重新获锁:前驱节点释放锁时唤醒该节点,线程竞争锁成功,重新设置 state 为之前的重入次数,从 await 方法返回,继续执行业务代码
相关推荐
程序员天天困1 小时前
Java 项目用 Single-flight 一招终结 AI 接口重复调用
redis·后端
farerboy1 小时前
23-Java 构造函数
java·后端
程序员麻辣烫1 小时前
Memory向量记忆系统3-数据选取
后端·aigc
程序员麻辣烫1 小时前
Memory向量记忆系统2-SQLite
后端·aigc
阳光是sunny1 小时前
LangGraph高级教程:Multi Schema多状态管理详解
前端·人工智能·后端
阳光是sunny2 小时前
LangGraph实战教程:状态管理与`graph.invoke`入参深度解析
前端·人工智能·后端
神奇小汤圆3 小时前
深入解析 Flink Kafka Connector:原理、配置与最佳实践
前端·后端
神奇小汤圆3 小时前
在 Nacos 点了下线,为什么流量还是打到了停机的机器上?
后端
阳光是sunny3 小时前
LangGraph实战教程:一文搞懂图的状态(State)管理
前端·人工智能·后端