2004年,Doug Lea在POC CSJP Workshop上发表了一篇题为《The java.util.concurrent Synchronizer Framework》的论文。这篇论文的核心贡献是提出了一个统一的同步器框架——AbstractQueuedSynchronizer(AQS),它用一个int类型的state变量一个FIFO队列,解决了Java并发编程中几乎所有同步原语的实现问题。

这个设计的结果是,JDK中超过一半的同步器都基于AQS实现:ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock、ThreadPoolExecutor的Worker、甚至FutureTask的早期版本。一个抽象类何以支撑起如此庞大的技术体系?答案藏在对"同步"本质的深刻洞察中。

同步器的本质:state与队列

Doug Lea在论文中指出,几乎所有同步器都遵循相同的模式:

acquire操作:
while (同步状态不允许获取) {
    将当前线程加入队列(如果尚未入队);
    阻塞当前线程;
}
将当前线程从队列中移除;

release操作:
更新同步状态;
if (新状态允许其他线程获取) {
    唤醒一个或多个队列中的线程;
}

这个模式揭示了同步器的两个核心组件:同步状态的管理线程的排队与唤醒。AQS的设计哲学是将这两个组件的通用逻辑抽取出来,形成框架;而将具体的状态判断逻辑留给子类实现。

state:一个变量的千变万化

AQS使用一个volatile修饰的int变量表示同步状态:

private volatile int state;

这个看似简单的设计却蕴含着巨大的灵活性:

  • ReentrantLock:state=0表示未锁定,state>0表示锁定次数(可重入)
  • CountDownLatch:state表示剩余计数,countDown()将其减1,await()等待state=0
  • Semaphore:state表示可用许可数量,acquire()减1,release()加1
  • ReentrantReadWriteLock:state的高16位表示读锁计数,低16位表示写锁计数

为什么选择int而非long或boolean?Doug Lea在论文中解释:J2SE 1.5时代,许多平台的64位CAS操作仍需通过内部锁模拟,性能不佳。32位足以满足大多数场景——唯一的例外是CyclicBarrier,它最终选择了基于锁的实现。

对state的访问通过三个final方法:

protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

CAS操作是整个框架的性能基石。在高并发场景下,CAS避免了锁的开销,使得无竞争的获取操作接近于直接内存访问的性能。

CLH队列:从自旋到阻塞的进化

当线程获取资源失败时,需要进入队列等待。AQS选择了CLH(Craig, Landin, and Hagersten)队列锁的变体作为等待队列的实现。

原始CLH队列

CLH队列最初是为自旋锁设计的。其核心思想是:每个等待线程持有一个节点,节点包含一个状态字段;线程自旋检查前驱节点的状态,而非竞争共享变量。

      head                    tail
        ↓                       ↓
    +-------+    +-------+    +-------+
    | Node  |───→| Node  |───→| Node  |
    |status |    |status |    |status |
    +-------+    +-------+    +-------+
        ↑            ↑            ↑
     Thread A     Thread B     Thread C
   (已获取锁)    (等待A释放)  (等待B获取后释放)

CLH队列的优势在于:入队和出队是O(1)操作,无锁且无阻塞;状态存储分散在各节点,减少了内存竞争。但它有一个致命缺陷:只适合自旋等待。

AQS的改进:双向队列与阻塞机制

Doug Lea对CLH队列做了两项关键改进:

第一,由单向队列改为双向队列。原始CLH队列中,节点只保存前驱指针。但对于阻塞式同步器,释放锁的线程需要主动唤醒后继节点——没有后继指针就无法高效找到唤醒目标。

第二,引入阻塞机制替代纯自旋。纯自旋在高竞争下会浪费大量CPU周期。AQS的策略是:先短暂自旋尝试,失败后调用LockSupport.park()阻塞线程。

改进后的队列结构:

CLH变体队列结构

图片来源: JavaGuide - AQS详解

每个Node节点包含:

字段 说明
thread 封装的线程引用
waitStatus 节点状态(SIGNAL/CONDITION/CANCELLED/PROPAGATE/0)
prev 前驱节点
next 后继节点
nextWaiter 条件队列中的后继节点

waitStatus状态机

waitStatus字段是一个精心设计的状态机:

状态 含义
0 新节点的初始状态
SIGNAL -1 后继节点需要被唤醒
CANCELLED 1 线程因中断或超时取消等待
CONDITION -2 节点在条件队列中等待
PROPAGATE -3 共享模式下传播唤醒信号

状态流转的核心是SIGNAL:新节点入队时,会将其前驱节点的状态从0改为SIGNAL,表示"我需要你唤醒"。当节点释放资源时,检查自己是否为SIGNAL状态,如果是,则唤醒后继节点并将状态重置为0。

独占模式:一次只有一个线程

独占模式是最直观的同步方式,同一时刻只有一个线程能获取资源。以ReentrantLock为例,其获取锁的流程:

// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) && 
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

流程分解:

1. tryAcquire:尝试获取

这是模板方法,由子类实现。非公平锁的实现:

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()) {
        // 可重入:state累加
        int nextc = c + acquires;
        setState(nextc);
        return true;
    }
    return false;
}

2. addWaiter:入队等待

获取失败后,将当前线程封装为Node节点加入队列尾部:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);  // CAS失败,自旋重试
    return node;
}

3. acquireQueued:队列中轮转

节点入队后,进入一个循环:如果前驱是头节点,则尝试获取;否则检查是否应该阻塞。

final boolean acquireQueued(final Node node, int arg) {
    for (;;) {
        final Node p = node.predecessor();
        if (p == head && tryAcquire(arg)) {
            setHead(node);  // 获取成功,成为新的头节点
            p.next = null;
            return false;
        }
        // 检查是否应该阻塞,并处理中断
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
            interrupted = true;
    }
}

AQS独占模式获取锁流程

图片来源: JavaGuide - AQS详解

释放锁的过程相对简单:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

unparkSuccessor从队列尾部向前遍历,找到第一个未取消的节点进行唤醒。为什么从尾部向前遍历?因为节点入队时,prev指针的设置早于next指针——从前往后遍历可能错过刚入队但next指针尚未设置的节点。

公平与非公平:吞吐量与公平性的权衡

ReentrantLock支持公平锁和非公平锁两种模式。它们的区别仅在于tryAcquire的实现:

非公平锁:直接尝试CAS获取,失败后才检查队列

公平锁:先检查队列中是否有等待更久的线程

// 公平锁的tryAcquire
if (c == 0) {
    if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

hasQueuedPredecessors()检查队列中是否存在有效节点。这个简单的判断带来了显著的性能差异。

Doug Lea在论文中给出了基准测试数据:在256个线程的极端竞争下,非公平锁的吞吐量是公平锁的10-100倍。原因在于:当持有锁的线程释放时,唤醒队列中的线程需要时间(操作系统调度开销);在这段时间内,新来的线程可以"插队"获取锁,避免了锁资源的空闲。

公平锁与非公平锁性能对比

图片来源: 美团技术团队 - AQS原理及应用

这就是"barging"策略:牺牲绝对的公平,换取更高的吞吐量。但公平锁仍有其用武之地——当锁持有时间较长、竞争不那么激烈时,公平锁可以避免某些线程长期饥饿。

共享模式:多个线程的并发访问

共享模式允许多个线程同时获取资源。以Semaphore为例,state表示可用许可数:

// Semaphore的非公平获取
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;  // 返回剩余资源数
    }
}

共享模式的核心复杂性在于传播唤醒:当一个线程释放资源后,可能需要唤醒多个后继节点。这引出了PROPAGATE状态——它解决了并发场景下可能出现的唤醒丢失问题。

考虑这个场景:2个资源,4个线程。T3、T4持有资源,T1、T2在队列等待。T3释放时唤醒T1,T1获取成功但还未完成setHead操作;此时T4也释放,发现head状态为0(T1还没来得及设置前驱状态),按照正常逻辑不会唤醒。如果没有PROPAGATE状态,T2就会永远等待。

PROPAGATE的设计确保:即使head状态被重置为0,也能通过状态判断需要继续传播唤醒。

Condition:条件变量的实现

AQS内部的ConditionObject类实现了条件变量语义。每个Condition维护一个独立的条件队列:

await()释放锁  加入条件队列  阻塞等待  被signal后加入同步队列  重新获取锁
signal()将条件队列头节点转移到同步队列

与Object.wait/notify不同,一个Lock可以关联多个Condition,实现更精细的线程协调。例如,生产者-消费者模型中可以使用两个Condition分别管理"缓冲区满"和"缓冲区空"的等待队列。

模板方法模式的精妙应用

AQS的设计是模板方法模式的经典应用。框架定义了acquire/release的骨架,子类只需实现以下钩子方法:

方法 说明
tryAcquire(int) 独占模式获取
tryRelease(int) 独占模式释放
tryAcquireShared(int) 共享模式获取
tryReleaseShared(int) 共享模式释放
isHeldExclusively() 是否独占持有

这种设计使得实现一个新的同步器变得异常简单。以下是一个最简Mutex的实现:

class Mutex {
    private static class Sync extends AbstractQueuedSynchronizer {
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, 1);
        }
        protected boolean tryRelease(int arg) {
            setState(0);
            return true;
        }
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }
    
    private final Sync sync = new Sync();
    public void lock() { sync.acquire(1); }
    public void unlock() { sync.release(1); }
}

十几行代码就实现了一个可用的互斥锁,底层自动获得队列管理、中断支持、超时机制等特性。

性能考量

Doug Lea在论文中给出了详细的性能测试数据:

无竞争开销(单线程,纳秒):

锁类型 开销
synchronized 58-189ns
Mutex (AQS) 71-137ns
ReentrantLock 81-140ns
Fair ReentrantLock 108-139ns

高竞争开销(256线程,纳秒):

锁类型 开销
synchronized 930-5214ns
Mutex (AQS) 798-667ns
ReentrantLock 843-188ns
Fair ReentrantLock 14967-15328ns

数据说明:非公平的AQS实现在高竞争下比synchronized快约一个数量级,而公平锁则慢一个数量级。这验证了barging策略的有效性。

结语

AQS的设计体现了一种深刻的工程智慧:将不变的部分固化在框架中,将变化的部分留给扩展者。一个int变量承载状态,一个队列管理等待,五个方法定义语义——这些简单的元素组合出了Java并发包中最丰富的同步原语生态。

Doug Lea在论文结尾写道:这个框架的成功在于它提供了足够高效的基础支持,使得"几乎从不需要从头实现同步器"。二十年后,这个判断依然成立。


参考资料

  1. Lea, D. (2004). The java.util.concurrent Synchronizer Framework. PODC CSJP Workshop.
  2. Craig, T. S. (1993). Building FIFO and priority-queueing spin locks from atomic swap. Technical Report TR 93-02-02, University of Washington.
  3. Magnusson, P., Landin, A., & Hagersten, E. (1994). Queue locks on cache coherent multiprocessors. 8th Int’l. Parallel Processing Symposium.
  4. Mellor-Crummey, J. M., & Scott, M. L. (1991). Algorithms for scalable synchronization on shared-memory multiprocessors. ACM Trans. on Computer Systems.
  5. Goetz, B., et al. (2006). Java Concurrency in Practice. Addison-Wesley.
  6. 美团技术团队. (2019). 从ReentrantLock的实现看AQS的原理及应用.
  7. JavaGuide. (2026). AQS详解.