3
0
0

Java 多线程:AQS 底层原理与同步队列

2026-11-03
2026-11-03
文章摘要
|

一、AQS 是什么?

1. AQS 解决什么问题?

假设有 100 个线程同时访问一个共享资源,需要实现:

  • 同一时间只允许一个线程获取锁。

  • 没有获取到锁的线程需要等待。

  • 持有锁的线程释放锁后,等待线程能够继续竞争。

  • 支持公平锁、非公平锁。

  • 支持可重入等特性。

如果每种锁都自己实现线程排队、阻塞和唤醒机制,会非常复杂。

因此,JDK 提供了 AQS。

AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 Java 并发包提供的一个同步器框架,负责管理线程竞争、排队、阻塞和唤醒。

AQS 本身不是一把具体的锁,而是一个可以被继承的同步器框架。

常见基于 AQS 实现的并发工具:

并发工具

作用

AQS 模式

ReentrantLock

可重入互斥锁

独占模式

ReentrantReadWriteLock

读写锁

独占 + 共享

CountDownLatch

等待多个任务完成

共享模式

Semaphore

控制并发访问数量

共享模式

可以理解为:

                  AQS
                   |
        ┌──────────┼──────────┐
        |          |          |
        v          v          v
  ReentrantLock  CountDownLatch  Semaphore
        |          |          |
     独占模式     共享模式     共享模式

AQS 提供线程同步的基础机制,具体的获取条件、释放条件和状态含义由子类定义。

2. AQS 的核心组成

AQS 最重要的两个部分:

第一:state 状态变量

用于记录当前同步状态。

例如:

  • ReentrantLock:记录锁的持有次数。

  • CountDownLatch:记录还剩多少任务没有完成。

  • Semaphore:记录还剩多少个许可。

第二:同步队列

当线程获取同步状态失败时,会进入同步队列等待。

AQS
 |
 |-- state:同步状态
 |
 |-- 同步队列:管理等待获取同步状态的线程
 |
 |-- Condition 队列:管理调用 await() 的线程

二、AQS 的 state 状态变量

1. state 是什么?

AQS 内部有一个非常重要的变量:

private volatile int state;

它是一个 volatile 修饰的 int。

AQS 提供了三个重要方法:

// 获取当前状态
protected final int getState();

// 设置状态
protected final void setState(int newState);

// CAS 修改状态
protected final boolean compareAndSetState(
        int expect,
        int update
);

这里联系之前学习的 CAS。

compareAndSetState() 就是基于 CAS 实现的状态更新。

注意:AQS 不规定 state 必须代表什么,它的具体含义由继承 AQS 的同步器决定。

2. 不同同步器的 state 有什么区别?

同步器

state 的含义

ReentrantLock

锁的持有次数

CountDownLatch

尚未完成的任务数量

Semaphore

当前可用许可数量

例如 ReentrantLock:

state = 0

表示当前没有线程持有锁。

线程 A 获取锁:

state = 1

表示线程 A 持有锁。

线程 A 再次获取同一把锁:

state = 2

表示线程 A 重入了两次。

线程 A 释放一次:

state = 1

锁仍然由线程 A 持有。

线程 A 再释放一次:

state = 0

锁完全释放。

所以:

对于 ReentrantLock 来说,state 不仅记录锁是否被占用,还记录当前线程重入的次数。

但不能将这个含义直接套用到 CountDownLatch 和 Semaphore。

另外,ReentrantReadWriteLock 比较特殊,它会将 state 的高 16 位用于记录读锁数量,低 16 位用于记录写锁重入次数。

三、AQS 同步队列:CLH 变体队列

1. 什么是同步队列?

假设线程 A 已经获得锁。

线程 B、C、D 也尝试获取同一把锁,但都失败了。

它们不能直接进入临界区,需要进入 AQS 的同步队列。

AQS 使用的是基于 CLH 锁队列思想改造的双向链表队列。

注意:它不是传统 CLH 自旋锁的简单实现,而是增加了线程阻塞、唤醒等机制。

队列结构:

Head(头节点)
  |
  v
Node B(等待线程 B)
  |
  v
Node C(等待线程 C)
  |
  v
Node D(等待线程 D)
  |
  v
Tail(尾节点)

其中:

  • Head:队列头节点。

  • Tail:队列尾节点。

  • Node:等待获取同步状态的节点。

AQS 的 Head 节点通常是一个哑节点,不代表正在等待的线程。

真正等待的第一个线程位于 Head 的后面。

当队列初始化时,会先创建一个空的 Head 节点,后续等待线程再依次加入。

2. Node 节点保存什么?

AQS 内部使用 Node 节点表示等待状态。

Java 8 中,Node 的核心字段:

字段

作用

waitStatus

当前节点的等待状态

prev

前驱节点

next

后继节点

thread

当前节点关联的线程

nextWaiter

共享模式标记或 Condition 队列关联

其中 waitStatus 比较重要。

Java 8 中常见状态:

状态

值

含义

CANCELLED

1

当前节点已经取消

SIGNAL

-1

后继节点需要被唤醒

CONDITION

-2

当前节点处于 Condition 等待队列

PROPAGATE

-3

共享模式下的传播标记

INITIAL

0

初始状态

重点理解两个:

SIGNAL:

表示当前节点的后继节点需要被通知。

例如:

Head → B → C

如果 C 因为无法获取锁而阻塞,B 需要记录相应的等待状态,以便后续正确唤醒 C。

CANCELLED:

表示当前节点已经取消,后续处理队列时需要跳过它。

四、线程获取锁失败后发生了什么?

这一部分是 AQS 的核心执行流程。

假设:

  • 线程 A 已经获得锁。

  • 线程 B 尝试获取锁。

  • 线程 B 获取失败。

AQS 内部的核心流程:

线程 B 调用 lock()
       |
       v
尝试获取同步状态
       |
       v
获取失败
       |
       v
创建 Node 节点
       |
       v
加入 AQS 同步队列
       |
       v
判断前驱节点是否为 Head
       |
       v
尝试再次获取锁
       |
       v
仍然失败
       |
       v
通过 LockSupport.park() 阻塞

1. 第一步:尝试获取锁

ReentrantLock 最终会调用 AQS 的 acquire 方法。

简化代码:

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {

        selfInterrupt();
    }
}

这里出现三个重要方法:

方法

作用

tryAcquire()

尝试获取同步状态

addWaiter()

创建节点并加入同步队列

acquireQueued()

在队列中等待并尝试获取同步状态

注意:

tryAcquire() 是留给具体同步器实现的模板方法,AQS 本身不规定如何获取锁。

2. 第二步:加入同步队列

如果线程获取锁失败,就需要进入同步队列。

例如:

当前队列:

Head
  |
  v
Node B
  |
  v
Node C

此时线程 D 也获取失败:

Head
  |
  v
Node B
  |
  v
Node C
  |
  v
Node D

AQS 通过 CAS 等机制更新 Tail,并维护节点之间的前后关系。

因此,多个线程同时入队时,也需要保证队列结构的并发安全。

3. 第三步:为什么要判断前驱节点是不是 Head?

线程 B 加入队列后,并不是一直等待。

它会尝试获取锁。

但是 AQS 有一个重要优化:

通常只有当前节点的前驱节点是 Head 时,才会尝试获取同步状态。

例如:

Head → B → C → D
  • B 的前驱是 Head,可以尝试获取锁。

  • C 的前驱是 B,不会直接尝试获取锁。

  • D 的前驱是 C,不会直接尝试获取锁。

这样可以减少多个排队线程同时竞争锁造成的额外开销。

如果 B 成功获得锁,它就会成为新的 Head:

旧 Head → B → C → D

获取成功后:

新 Head(B) → C → D

B 的节点成为新的哑节点。

4. 第四步:为什么要 park?

如果线程仍然无法获得锁,就需要阻塞等待。

AQS 使用:

LockSupport.park();

将当前线程挂起。

例如:

Head → B → C → D

B:等待获取锁

C:等待获取锁

D:等待获取锁

当线程无法获得锁时,可以通过 park 减少无意义的 CPU 自旋。

这里要区分:

方法

作用

CAS

原子更新同步状态

park()

阻塞线程

unpark()

唤醒线程

它们是不同的机制。

5. LockSupport 是什么?

LockSupport 是 JDK 提供的线程阻塞工具类。

常见方法:

LockSupport.park();

LockSupport.unpark(thread);

可以将它理解为每个线程关联一个许可。

  • park():没有许可时阻塞,有许可时消费许可并继续。

  • unpark(thread):给指定线程提供许可。

一个非常重要的特点:

unpark() 可以先于 park() 执行,许可不会因为线程尚未调用 park() 就丢失。

这也是它适合用于 AQS 阻塞和唤醒机制的原因之一。

另外,park() 可能因为中断或伪唤醒而返回,因此 AQS 必须在循环中重新检查获取条件,不能认为 park() 返回就代表已经获得锁。

五、释放锁后,AQS 如何唤醒线程?

假设:

Head → B → C → D

线程 A 当前持有锁。

当 A 执行:

lock.unlock();

AQS 会执行释放逻辑。

简化流程:

线程 A 调用 unlock()
       |
       v
执行 tryRelease()
       |
       v
判断锁是否完全释放
       |
       v
完全释放
       |
       v
唤醒同步队列中的后继节点
       |
       v
线程 B 被唤醒
       |
       v
线程 B 再次尝试获取锁
       |
       v
获取成功,成为新的 Head

1. tryRelease()

AQS 的 release 方法:

public final boolean release(int arg) {

    if (tryRelease(arg)) {

        Node h = head;

        if (h != null && h.waitStatus != 0) {
            unparkSuccessor(h);
        }

        return true;
    }

    return false;
}

这里有一个重要细节:

只有 tryRelease() 返回 true,才表示同步状态已经完全释放,可以唤醒后继节点。

以 ReentrantLock 为例:

假设当前线程重入两次:

state = 2

第一次 unlock:

state = 1

锁还没有完全释放,因此不需要唤醒等待线程。

第二次 unlock:

state = 0

锁完全释放,才会唤醒等待线程。

2. unparkSuccessor()

AQS 会通过:

LockSupport.unpark(thread);

唤醒合适的后继线程。

被唤醒的线程并不意味着已经获得锁。

它只是从阻塞状态恢复,继续尝试获取同步状态。

因此:

unpark 只是唤醒,不等于获取锁成功。

如果其他线程抢先获得锁,被唤醒的线程仍然需要继续等待。

3. AQS 是严格公平的吗?

不是。

AQS 的同步队列通常按照入队顺序组织等待节点,但具体同步器是否按照队列顺序获取资源,还要看实现策略。

例如 ReentrantLock:

  • 非公平锁:允许新线程直接尝试抢锁,可能插队。

  • 公平锁:获取锁时会考虑是否存在排在前面的等待线程。

因此:

AQS 使用队列,不代表基于 AQS 的所有锁都是公平锁。

六、AQS 的两种模式:独占模式与共享模式

这是理解 AQS 各种并发工具的关键。

1. 独占模式(Exclusive)

独占模式表示:

同一时刻只允许一个线程成功持有同步资源。

例如 ReentrantLock:

ReentrantLock lock = new ReentrantLock();

lock.lock();

try {
    // 同一时刻只允许一个线程持有这把锁
} finally {
    lock.unlock();
}

如果线程 A 已经获得锁,线程 B、C 就需要等待。

对应的 AQS 方法主要有:

acquire();

release();

独占模式的典型实现:

  • ReentrantLock。

  • ReentrantReadWriteLock 的写锁。

2. 共享模式(Shared)

共享模式表示:

允许多个线程同时获取同步资源,但需要满足同步器规定的条件。

例如 Semaphore:

Semaphore semaphore = new Semaphore(3);

表示最多允许三个线程同时获取许可。

Semaphore 许可数量:3

线程 A 获取许可:剩余 2

线程 B 获取许可:剩余 1

线程 C 获取许可:剩余 0

线程 D 获取失败,进入等待

当某个线程释放许可后,等待线程就有机会获取许可。

对应 AQS 方法主要有:

acquireShared();

releaseShared();

共享模式的典型实现:

  • CountDownLatch。

  • Semaphore。

  • ReentrantReadWriteLock 的读锁。

3. 独占与共享的区别

对比

独占模式

共享模式

资源获取

独占

可以共享

同时成功的线程

通常一个

可以多个

典型实现

ReentrantLock

Semaphore

AQS 获取方法

acquire()

acquireShared()

AQS 释放方法

release()

releaseShared()

这里还有一个容易混淆的地方:

读写锁并不是两把完全无关的锁。

ReentrantReadWriteLock 使用 AQS 的共享模式管理读锁,使用独占模式管理写锁。

因此可以实现:

  • 读读共享。

  • 读写互斥。

  • 写写互斥。

4. Condition 队列与同步队列有什么区别?

AQS 除了同步队列,还支持 Condition 条件队列。

两者不是同一个队列。

对比

同步队列

Condition 队列

作用

等待获取锁或同步状态

等待某个条件成立

典型操作

acquire、park

await、signal

节点关系

双向链表

单向链表

进入方式

获取同步状态失败

调用 await()

后续去向

竞争同步状态

signal 后转入同步队列

例如,线程调用 Condition.await():

  1. 当前线程必须先持有对应的锁。

  2. 调用 await() 后释放当前持有的锁。

  3. 线程进入 Condition 等待队列。

  4. 其他线程调用 signal() 后,将等待节点转移到 AQS 同步队列。

  5. 被唤醒的线程重新竞争锁。

  6. 成功获取锁后,await() 才返回。

重点:

signal() 不是直接让等待线程获得锁,而是让它有机会重新进入同步队列竞争锁。

七、ReentrantLock 如何基于 AQS 实现?

1. ReentrantLock 的底层结构

简化结构:

ReentrantLock
      |
      v
Sync(继承 AQS)
      |
      ├── NonfairSync(非公平锁)
      |
      └── FairSync(公平锁)

ReentrantLock 内部通过 Sync 继承 AQS,实现不同的加锁策略。

2. ReentrantLock 获取锁的过程

例如:

ReentrantLock lock = new ReentrantLock();

lock.lock();

执行过程:

lock.lock()
     |
     v
进入 AQS acquire(1)
     |
     v
tryAcquire(1)
     |
     v
判断 state
     |
     ├── state == 0
     |       |
     |       v
     |   CAS 尝试获取锁
     |       |
     |       v
     |   获取成功
     |
     ├── 当前线程已经持有锁
     |       |
     |       v
     |   state + 1
     |
     └── 其他线程持有锁
             |
             v
         加入同步队列
             |
             v
         park 等待

3. ReentrantLock 为什么支持可重入?

因为它会记录当前锁的持有线程。

例如:

ReentrantLock lock = new ReentrantLock();

lock.lock();

try {

    lock.lock();

    try {
        // 第二次获取同一把锁
    } finally {
        lock.unlock();
    }

} finally {
    lock.unlock();
}

第一次获取:

state = 1

第二次获取:

state = 2

第一次释放:

state = 1

第二次释放:

state = 0

锁完全释放。

4. 公平锁和非公平锁如何实现?

创建方式:

// 非公平锁,默认方式
ReentrantLock lock1 = new ReentrantLock();

// 公平锁
ReentrantLock lock2 = new ReentrantLock(true);

非公平锁:

新线程可以直接尝试 CAS 获取锁,即使队列中已经存在等待线程,也可能抢先成功。

公平锁:

获取锁时会先检查是否存在排在前面的等待线程,尽量按照排队顺序获取。

注意:

公平锁通常吞吐量较低,不代表绝对不会发生线程调度层面的延迟。

生产环境应根据业务的公平性要求和吞吐量需求选择。

八、Java 8 实战:自定义一个简单的 AQS 独占锁

前面介绍的都是原理,现在通过一个简单的自定义同步器,真正理解 AQS 如何工作。

下面实现一个不支持重入的互斥锁。

注意:这个示例用于学习 AQS,不建议在生产项目中自行实现通用锁,生产环境优先使用 JDK 提供的 ReentrantLock。

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class AqsMutex {

    private static class Sync
            extends AbstractQueuedSynchronizer {

        // 尝试获取锁
        @Override
        protected boolean tryAcquire(int arg) {

            // state 为 0,表示锁空闲
            if (compareAndSetState(0, 1)) {

                setExclusiveOwnerThread(
                        Thread.currentThread()
                );

                return true;
            }

            return false;
        }

        // 尝试释放锁
        @Override
        protected boolean tryRelease(int arg) {

            // 只能由持有锁的线程释放
            if (Thread.currentThread()
                    != getExclusiveOwnerThread()) {

                throw new IllegalMonitorStateException();
            }

            setExclusiveOwnerThread(null);

            setState(0);

            return true;
        }
    }

    private final Sync sync = new Sync();

    public void lock() {

        sync.acquire(1);
    }

    public void unlock() {

        sync.release(1);
    }
}

使用:

public class AqsMutexDemo {

    private static int count = 0;

    private static final AqsMutex LOCK =
            new AqsMutex();

    public static void main(String[] args)
            throws InterruptedException {

        Runnable task = () -> {

            for (int i = 0; i < 10000; i++) {

                LOCK.lock();

                try {
                    count++;
                } finally {
                    LOCK.unlock();
                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count);
    }
}

预期结果:

20000

代码解析:

代码

作用

extends AbstractQueuedSynchronizer

继承 AQS

compareAndSetState(0, 1)

CAS 获取锁

tryAcquire()

定义获取锁的规则

tryRelease()

定义释放锁的规则

acquire(1)

调用 AQS 获取同步状态

release(1)

调用 AQS 释放同步状态

最重要的是:

我们只需要实现 tryAcquire() 和 tryRelease(),AQS 就帮我们处理线程排队、阻塞和唤醒。

这就是 AQS 模板方法设计模式的核心思想。

注意:

  • 这个 Mutex 是非公平、不可重入的示例锁。

  • 同一个线程重复调用 lock(),第二次会等待自己释放锁,造成自我阻塞。

  • 生产代码应优先使用 ReentrantLock,不要随意自行实现通用同步器。

九、重点总结

知识点

核心结论

AQS

Java 并发同步器框架

state

表示同步状态,由子类定义具体含义

CAS

原子更新 AQS 同步状态

同步队列

管理获取同步状态失败的线程

CLH 变体

AQS 使用改造后的双向队列

Head

同步队列头节点,通常是哑节点

Tail

同步队列尾节点

Node

保存线程、前后节点和等待状态

LockSupport.park()

阻塞线程

LockSupport.unpark()

唤醒线程

tryAcquire()

定义独占模式获取规则

tryRelease()

定义独占模式释放规则

独占模式

同一时刻一个线程持有同步资源

共享模式

允许多个线程同时获取资源

ReentrantLock

基于 AQS 独占模式实现

Semaphore

基于 AQS 共享模式实现

CountDownLatch

基于 AQS 共享模式实现

需要重点记住:

  1. AQS 本身不是锁,而是构建同步器的框架。

  2. state 是 AQS 的核心状态变量,其含义由具体同步器决定。

  3. 获取同步状态失败的线程会进入同步队列。

  4. AQS 使用 LockSupport 实现线程阻塞和唤醒。

  5. 被 unpark 唤醒不代表已经获得锁,还需要重新竞争。

  6. AQS 支持独占模式和共享模式。

  7. ReentrantLock 通过继承 AQS 实现加锁、排队、重入和释放。

  8. 公平与非公平取决于具体同步器的获取策略,AQS 队列本身不保证绝对公平。

  9. Condition 条件队列与 AQS 同步队列是两个不同的队列。

十、常见问题与答案

1. AQS 是什么?

AQS 是 Java 并发包中的抽象队列同步器,提供同步状态管理、线程排队、阻塞和唤醒等基础机制。

2. AQS 的 state 是什么?

state 是同步状态变量,由 volatile 修饰。具体含义取决于同步器,例如 ReentrantLock 使用它记录重入次数,Semaphore 使用它记录许可数量。

3. 为什么 AQS 使用双向链表?

双向链表可以方便地维护前驱和后继节点,支持节点入队、取消和唤醒后继线程等操作。

4. 为什么获取锁失败后不一直 CAS?

持续自旋会消耗 CPU。AQS 在竞争失败后会通过 LockSupport.park() 阻塞线程,减少 CPU 资源浪费。

5. unpark() 后线程一定获得锁吗?

不一定。unpark() 只是唤醒线程,线程恢复后仍然需要重新竞争同步状态。

6. AQS 是公平的吗?

AQS 本身不保证公平。具体同步器可以实现公平或非公平的获取策略。

7. ReentrantLock 为什么支持可重入?

因为它会记录锁的持有线程,并使用 state 记录重入次数。同一个线程再次获取锁时,state 增加。

8. CountDownLatch 为什么属于共享模式?

因为多个线程可以同时等待计数器归零。当计数器归零时,多个等待线程可以被唤醒。

9. Semaphore 为什么属于共享模式?

因为它管理的是许可数量,允许多个线程同时获得许可,只要许可数量满足条件。

10. AQS 和 CAS 有什么关系?

CAS 是 AQS 实现同步状态原子更新的重要基础机制。AQS 使用 CAS 竞争和修改 state,同时使用同步队列管理失败的线程。

11. Condition 队列和同步队列有什么区别?

同步队列用于等待获取锁;Condition 队列用于等待特定条件。调用 signal() 后,Condition 等待节点会转移到同步队列,之后重新竞争锁。

十一、复习 测试

  1. AQS 的 state 变量主要有什么作用?

  2. 为什么 ReentrantLock 的 state 可以从 0 变成 1,再变成 2?

  3. AQS 的同步队列为什么叫 CLH 变体队列?

  4. Head 节点是不是正在等待获取锁的第一个线程?

  5. Node 的 SIGNAL 和 CANCELLED 分别代表什么?

  6. 线程获取锁失败后,为什么需要加入同步队列?

  7. 为什么通常只有前驱节点是 Head 的线程才尝试获取锁?

  8. LockSupport.park() 和 unpark() 分别有什么作用?

  9. 线程被 unpark() 唤醒后,是否意味着已经获得锁?

  10. ReentrantLock 释放一次锁后,为什么不一定唤醒其他线程?

  11. AQS 的独占模式和共享模式有什么区别?

  12. Condition 队列与同步队列有什么区别?

  13. ReentrantLock 为什么能够基于 AQS 实现可重入?

  14. 公平锁与非公平锁在获取锁时有什么区别?

  15. 自定义 AQS 同步器时,tryAcquire() 和 tryRelease() 分别负责什么?

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论