Java并发编程进阶:彻底理解 AQS 底层原理

一、AQS整体结构

AQS(AbstractQueuedSynchronizer)是 JDK 并发包 java.util.concurrent.locks 的核心同步框架。

它本身并不实现具体锁,而是提供了一套通用机制:

  • 状态管理(state)
  • 线程阻塞与唤醒
  • FIFO同步队列

很多并发组件都基于 AQS 实现:

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore
  • CountDownLatch

整体结构:

AQS的设计思想:

state负责资源状态

获取失败的线程进入队列

前驱节点释放资源后唤醒后继节点

FIFO保证公平性(公平锁)

二、核心成员变量

AQS最重要的成员变量只有三个:

java 复制代码
private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

1、state同步状态

java 复制代码
private volatile int state;

AQS所有同步器本质都围绕state展开

例如:

ReentrantLock

state = 0 表示无锁
state = 1 表示已经被占用
state = 5 表示重入5次

Semaphore

state = 3 表示还有3个许可证

CountDownLatch

state = 10 表示还有10个任务未完成

修改state全部依赖CAS:

java 复制代码
compareAndSetState(expect, update)

底层:

java 复制代码
Unsafe.compareAndSwapInt(...)

2、head 同步队列头节点

java 复制代码
private transient volatile Node head;

特点:

head通常是哨兵节点

不对应真实线程

真正等待线程从head.next开始

例如:

head -> T1 -> T2 -> T3

head为空节点。

3、tail 同步队列尾节点

java 复制代码
private transient volatile Node tail;

所有新节点都从尾部插入形成双向链表:

head -> T1 -> T2 -> T3

tail

三、AQS在JUC中的位置

AQS是锁吗?

为什么源码里总看到sync.acquire()?

实际上:

AQS 并不是锁

它只是 JDK 提供的一个同步器框架,用来解决以下通用问题:

  • 线程排队
  • 线程阻塞
  • 线程唤醒
  • 同步状态管理

真正对外提供锁功能的是 ReentrantLock

整体关系如下:

复制代码
                Lock接口
                    ▲
                    │
             ReentrantLock
                    │
                    ▼
                  Sync
                    │
                    ▼
     AbstractQueuedSynchronizer

源码:

java 复制代码
public class ReentrantLock implements Lock {

    private final Sync sync;

}

其中:

java 复制代码
abstract static class Sync
        extends AbstractQueuedSynchronizer {
}

说明:

复制代码
ReentrantLock
        ↓
      Sync
        ↓
      AQS

AQS负责同步框架

Sync负责具体同步规则

ReentrantLock负责向外提供 lock()/unlock() API

四、AQS的设计模式------模板方法模式

AQS最大的设计亮点就是模板方法模式

AQS规定了:

acquire()

release()

整个获取资源和释放资源的流程
tryAcquire()

tryRelease()

交给子类实现

例如:

java 复制代码
public final void acquire(int arg) {

    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire(arg)

实际上会执行子类实现的方法

例如:

java 复制代码
ReentrantLock.Sync.tryAcquire()

因此:

复制代码
AQS负责:

    怎么排队
    怎么阻塞
    怎么唤醒

子类负责:

    什么情况下获取成功
    什么情况下释放成功

结构如下:

AQS

acquire()

release()

tryAcquire()

tryRelease()

ReentrantLock

Semaphore

CountDownLatch

这也是 AQS 能够支撑大量同步器实现的根本原因

五、AQS 是干什么的

AQS(AbstractQueuedSynchronizer)看着很复杂,实际上它只解决一个问题:

多个线程竞争资源时,如何管理等待线程?

例如:

java 复制代码
ReentrantLock lock = new ReentrantLock();

lock.lock();

假设:

Thread-A 获取成功

Thread-B 获取失败

Thread-C 获取失败

会产生一下问题:

B怎么办?

C怎么办?

谁先获得锁?

怎么挂起?

怎么唤醒?

这些事情全部由 AQS 完成,所以可以先记住:

AQS = 锁的排队管理器

AQS只管理两样东西:

state

队列

结构:

javascript 复制代码
          AQS
           │
    ┌──────┴──────┐
    │             │
 state          Queue

1、state表示资源状态

java 复制代码
private volatile int state;

例如:

ReentrantLock

state = 0 表示无锁
state = 1 表示已经被占用
state = 5 表示重入5次

Semaphore

state = 3 表示还有3个许可证

CountDownLatch

state = 10 表示还有10个任务未完成

因此可以理解为state = 资源数量

2、Queue

获取资源失败的线程进入队列等待

例如:

复制代码
Thread-A 获取锁

Thread-B 失败

Thread-C 失败

形成:

复制代码
head -> B -> C

六、为什么需要 state

假设没有 state

线程来了:

A 获取锁

系统根本不知道锁是否被占用

所以需要一个变量记录

java 复制代码
volatile int state;

例如

state=0表示没人占用

java 复制代码
state = 0

state=1表示有人占用

复制代码
state = 1

线程获取锁:

复制代码
CAS(state,0,1)

成功则修改state=1代表拿到锁

失败则进入队列

所以:

复制代码
state负责资源管理

七、为什么需要队列?

假设:A 持有锁

此时:B来、C来、D来 ------> 全部失败

如果没有队列:

B一直循环抢锁

C一直循环抢锁

D一直循环抢锁

CPU直接爆炸,所以:

抢不到

排队

睡觉

形成:

java 复制代码
head -> B -> C -> D

这就是 AQS 队列存在的意义

八、AQS队列是什么?

AQS采用:

CLH变体双向队列

源码:

java 复制代码
private transient volatile Node head;
private transient volatile Node tail;

队列结构:

复制代码
head
 ↓
Node(B)
 ↓
Node(C)
 ↓
Node(D)

实际上是双向链表:

复制代码
head <-> B <-> C <-> D

每个节点:

java 复制代码
static final class Node {

    Node prev;

    Node next;

    Thread thread;
}

保存:

当前线程

前驱节点

后继节点

九、获取锁流程

例如ReentrantLock:

java 复制代码
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

进入:

java 复制代码
acquire(1);

流程:

java 复制代码
tryAcquire()

尝试抢锁

成功:结束

失败:创建Node

入队:

head -> B -> C

挂起------>线程睡觉:

java 复制代码
LockSupport.park();

十、为什么不是立即抢锁

假设:

复制代码
head -> B -> C -> D

锁释放后

如果B C D一起抢,导致CPU竞争非常激烈

AQS规定:

只有head.next

才能尝试获取锁

也就是说只有B能抢

源码:

java 复制代码
if (p == head &&
    tryAcquire(arg))

这里p == head

就是我是队首

这样减少大量竞争

十一、释放锁流程

例如:

java 复制代码
ReentrantLock.unlock();
java 复制代码
public void unlock() {
        sync.release(1);
    }

进入release方法:

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

调用:tryRelease()方法修改state
例如 state = 0

然后 unparkSuccessor(head);

唤醒 head.next

例如:

head -> B -> C -> D
唤醒:B

B被唤醒后:

成功:成为新head

结果:head -> C -> D