Java并发核心:你以为AQS很复杂?无非是"两个队列"和"一个状态"

前言

上周面试,面试官问我:"你能说说 Java 的AQS基于管程是如何实现的吗?" 我当时只背了概念,结果当场翻车。回来后我花了 3 天,把 AQS源码啃了一遍,整理出这篇能直接拿去面试的笔记。

1、什么是管程

管程是基于MESA模型实现的,管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量

模型如下图所示:

⚠️提醒

✔️入口等待队列:多线程进入的时排队,只允许一个线程进入管程内部,其他线程等待

✔️条件变量和等待队列:解决线程同步的问题

2、Java 中管程(Monitor)的实现

  • 其一是基于Object监视器(Monitor)机制的内置synchronized同步;

[

  • 其二是基于抽象队列同步器(AQS)构建的java.util.concurrent.locks.Lock显式锁机制。

一、AQS原理分析

1、什么是AQS

java.util.concurrent 包中的同步器大多构建在一些共同的基础行为之上,例如等待队列、条件队列、独占获取与共享获取等。这些行为被抽象为一个统一的框架------AbstractQueuedSynchronizer(简称 AQS)

AQS 是一个用于实现依赖状态型同步器的抽象同步框架,为构建各种同步机制提供了基础支持。

2、AQS实现方式

  • 一般是通过一个内部类Sync继承 AQS
  • 将同步器所有调用都映射到Sync对应的方法

2.1、举个例子 🌰🌰 (ReentrantLock)

scala 复制代码
public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
......
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {}
......
}

3、AQS具备的特性

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

二、AQS核心结构

1、 AQS核心源码

csharp 复制代码
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }
.....    
    //链表头节点
    private transient volatile Node head;

    // 链表尾节点
    private transient volatile Node tail;

    //共享变量,使用volatile修饰保证线程可见性
    private volatile int state;

    //获取状态
    protected final int getState() {
        return state;
    }

    //设置状态
    protected final void setState(int newState) {
        state = newState;
    }

    //CAS操作:将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
    //stateOffset:就是定义的state值
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
.....
}

⚠️AQS 的关键实现基于以下两点核心机制:

✔️ 关键字段的可见性

在 AQS 源码中,用于表示同步状态的 state、以及等待队列的头尾指针 headtail 均被声明为 volatile,确保这些状态在多线程之间的立即可见性。

✔️ 基于 CAS 的无锁竞争设计

获取锁的核心逻辑依赖于对 state 字段的 CAS(Compare-And-Swap)操作,通过这一原子性操作实现无锁化的线程竞争与状态更新,从而避免传统锁机制带来的阻塞开销。

2、AQS两种队列

两种队列结构示意图

2.1、同步等待队列

负责管理在竞争锁失败时进入等待状态的线程,基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制

2.1.1、公平锁

新线程到来时,会先检查同步队列中是否有已在等待的线程。只要有排队者,它就会直接进入队列尾部等待,不会尝试获取锁。这是其"公平"的核心。如果队列为空,它仍会尝试获取锁。

2.1.2、非公平锁

新线程到来时,无论队列是否为空,都会先尝试一次CAS操作去"插队"抢锁。只有抢锁失败后,才会进入队列尾部排队。

⚠️提示
✔️AQS默认采用非公平策略,给予了新线程一次"插队"的机会,旨在减少线程切换频率、提升整体吞吐性能。
✔️线程进入同步队列后,会因无法立即获取锁而发生实际的挂起与唤醒,这一过程涉及内核态切换与上下文恢复,因而存在一定的性能开销

2.2、条件等待队列

与特定条件关联,当线程调用 await() 时,会释放已持有的锁并进入条件队列等待

当其他线程调用 signal() 唤醒时,该队列中的线程结点会被移至同步等待队列,重新参与锁的竞争

2.2.1、 条件队列源码

条件队列唤醒

java 复制代码
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
......
        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
       // 唤醒
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
    /**
     * 将节点从条件队列转移到同步队列
     */
    final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
}

条件队列转换为同步队列

ini 复制代码
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

⚠️注意

✔️基于LockSupport.unpark(node.thread)、LockSupport.park(node.thread) 实现阻塞和唤醒

✔️条件队列在AQS内部是一个单向链表,节点在被转移到同步队列时,被构建为双向链表节点CLH队列

2.3、区别对比

特性 条件等待队列 同步等待队列
数据结构 单向链表 双向链表
连接指针 nextWaiter prev, next
主要操作 await(尾部添加),signal(头部移除) acquire(尾部添加/自旋),release(头部唤醒/移除)
关键需求 简单的 FIFO 等待/唤醒 必须支持从任意位置取消节点、状态传播、稳定的遍历
设计目标 实现简单,节省资源 功能完备,操作高效且安全

⚠️注意

✔️节点从单向的条件队列 转移到双向的同步队列 时,其数据结构确实发生了改变,这是由两个队列所承担的不同职责和操作需求决定的。

✔️这种设计是AQS既能保持高效,又能支持复杂同步语义(如超时、中断、共享模式)的基础之一。


三、总结

AQS不是对管程的简单模仿,而是用轻量级队列算法在 Java 语言层面重新发明了管程。它将操作系统 / JVM 的管程概念抽象为可组合的 Java 组件,使得开发者能够以极低的成本构建出高性能、高可控的同步工具。

相关推荐
Rust研习社11 分钟前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒35 分钟前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro1 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax2 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH2 小时前
Koa和Express的区别
后端
MariaH2 小时前
Koa框架的使用
后端
luckdewei3 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某4 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy4 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom5 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github