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

0 阅读6分钟

前言

上周面试,面试官问我:“你能说说 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)

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核心源码

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、以及等待队列的头尾指针 head 和 tail 均被声明为 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、 条件队列源码

条件队列唤醒

    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;
    }
}

条件队列转换为同步队列

    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、区别对比

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

⚠️注意
✔️节点从单向的条件队列转移到双向的同步队列时,其数据结构确实发生了改变,这是由两个队列所承担的不同职责和操作需求决定的。
✔️这种设计是AQS既能保持高效,又能支持复杂同步语义(如超时、中断、共享模式)的基础之一。


三、总结

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