Java并发编程基石:深入解析AQS原理与应用实战

toc

1 AQS概述:并发编程的基石

AbstractQueuedSynchronizer(AQS)是Java并发包(java.util.concurrent.locks)的核心基础框架,为构建锁和同步器提供了可重用的基础设施。AQS通过模板方法模式让开发者能够基于状态和队列轻松构建各种同步器,如ReentrantLock、Semaphore、CountDownLatch等常见并发工具。

AQS的核心设计思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效工作线程,并将共享资源设置为锁定状态;如果共享资源被占用,则需要一套线程阻塞等待以及被唤醒时锁分配的机制。这套机制通过CLH队列锁实现,将暂时获取不到锁的线程加入到队列中。

与传统的synchronized关键字相比,AQS具有显著优势:它由Java语言实现(而非C++),提供了更灵活的API,并且在锁竞争激烈的情况下提供了多种解决方案,性能表现更优。

2 AQS核心原理与架构设计

2.1 核心组件:状态变量与同步队列

AQS的核心架构围绕两个关键组件构建:同步状态(state)等待队列

**同步状态(state)**是一个volatile int类型的变量,表示共享资源的状态。其具体语义由子类定义,例如:

  • 在ReentrantLock中,state表示锁的重入次数
  • 在Semaphore中,state表示可用的许可证数量
  • 在CountDownLatch中,state表示未完成的计数

AQS提供了三种原子操作方法来管理state:

java 复制代码
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
    // CAS操作,保证原子性
}

等待队列 是AQS管理的CLH(Craig, Landin, and Hagersten)队列的变体,是一个FIFO双向链表,用于存储等待获取锁的线程。队列中的每个节点(Node)包含线程引用、等待状态以及前后指针。

2.2 节点状态与队列管理

AQS队列中的节点存在多种等待状态,决定了线程的排队行为:

  • CANCELLED(1):节点因超时或中断被取消,不再参与同步
  • SIGNAL(-1):后继节点需要被唤醒
  • CONDITION(-2):节点在条件队列中等待
  • PROPAGATE(-3):共享模式下状态变更需要传播
  • 0:初始状态

AQS通过精细的队列管理机制实现高效的线程调度。当线程获取锁失败时,会被包装成Node节点加入队列尾部;当锁释放时,会唤醒队列中的合适节点。

3 AQS的两种同步模式

3.1 独占模式(Exclusive Mode)

独占模式下,同一时刻只有一个线程能获取同步状态,其他线程必须等待。ReentrantLock是独占模式的典型实现。

获取锁流程(acquire)

  1. 调用tryAcquire()尝试直接获取资源(子类实现)
  2. 若成功,则线程继续执行
  3. 若失败,将线程包装为独占节点(Node.EXCLUSIVE)加入队列尾部
  4. 线程在队列中自旋或阻塞,直到被前驱节点唤醒
  5. 被唤醒后,检查前驱是否为头节点,是则尝试获取锁

释放锁流程(release)

  1. 调用tryRelease()尝试释放资源(子类实现)
  2. 若释放成功(state=0),检查后继节点是否需要唤醒
  3. 唤醒后继节点中的线程

可重入性是独占模式的重要特性,通过state计数实现------每次重入state加1,释放时相应减1。

3.2 共享模式(Shared Mode)

共享模式下,多个线程可以同时获取同步状态,适用于资源限流等场景。Semaphore、CountDownLatch是共享模式的典型实现。

获取共享资源流程

  1. 调用tryAcquireShared()尝试获取资源
  2. 返回值表示获取结果:负数表示失败;0表示成功但无剩余资源;正数表示成功且有剩余资源
  3. 若失败,线程加入等待队列
  4. 成功获取资源后,若还有剩余资源,会唤醒后续共享节点

释放共享资源流程

  1. 调用tryReleaseShared()释放资源
  2. 若释放成功,唤醒所有可获取资源的后继节点

共享模式支持传播机制,当资源可用时,会连续唤醒多个等待线程,提高吞吐量。

3.3 公平与非公平策略

AQS支持公平与非公平两种调度策略:

  • 公平锁:严格按照FIFO顺序分配锁,保证无饥饿现象
  • 非公平锁:新线程可插队尝试获取锁,提高吞吐量但可能导致饥饿

ReentrantLock的两者实现差异主要在于tryAcquire()方法:公平锁先检查是否有等待线程,有则排队;非公平锁直接尝试CAS获取锁。

4 AQS源码关键流程解析

4.1 独占模式获取资源源码分析

java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) && 
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

addWaiter方法将当前线程包装成Node节点并插入队列尾部:

  • 使用CAS操作确保尾节点插入的原子性
  • 队列为空时进行初始化

acquireQueued方法是线程在队列中的核心逻辑:

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

此方法通过自旋方式,只有当前节点的前驱是头节点时才尝试获取锁,避免不必要的竞争。获取成功后,将当前节点设为新头节点(旧头节点出队)。

4.2 释放资源与唤醒机制

释放资源时,AQS调用unparkSuccessor()唤醒合适的后继线程:

  • 从队列尾部向前遍历,找到离头节点最近的非取消状态节点
  • 通过LockSupport.unpark()唤醒对应线程

这种后向遍历机制确保即使存在取消节点也能找到有效后继。

5 AQS在Java并发工具中的应用

5.1 标准同步器实现对比

同步器 同步模式 state含义 特性
ReentrantLock 独占 重入次数 支持公平/非公平锁
Semaphore 共享 可用许可证数量 控制并发线程数
CountDownLatch 共享 未完成的任务数 一次性屏障,不可重置
ReentrantReadWriteLock 组合 高16位读锁,低16位写锁 读写分离,写锁优先

5.2 ReentrantLock实现原理

ReentrantLock通过内部类Sync(继承AQS)实现同步机制。其公平与非公平策略分别由FairSync和NonfairSync实现。

非公平锁尝试获取锁时直接CAS操作:

java 复制代码
protected final boolean tryAcquire(int acquires) {
    // 非公平尝试,直接插队
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

而公平锁先检查队列:

java 复制代码
protected final boolean tryAcquire(int acquires) {
    // 先检查是否有等待线程
    if (hasQueuedPredecessors())
        return false;
    // ...尝试CAS获取
}

5.3 读写锁实现技巧

ReentrantReadWriteLock使用state的高16位记录读锁数量,低16位记录写锁重入次数。这种位运算技巧使单个state变量能同时管理两种锁状态。

读锁(共享锁)允许多线程同时访问,写锁(独占锁)保证互斥访问。当写锁持有时,所有读锁请求被阻塞;当读锁持有时,写锁请求被阻塞。

6 AQS实战:自定义同步器开发

6.1 实现简单的互斥锁

以下基于AQS实现简易互斥锁:

java 复制代码
public class MyMutex {
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int acquires) {
            // CAS将state从0改为1表示获取锁
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        
        @Override
        protected boolean tryRelease(int releases) {
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        
        public Condition newCondition() {
            return new ConditionObject();
        }
    }
    
    private final Sync sync = new Sync();
    
    public void lock() { sync.acquire(1); }
    public void unlock() { sync.release(1); }
    public boolean isLocked() { return sync.isHeldExclusively(); }
    public Condition newCondition() { return sync.newCondition(); }
}

此实现演示了AQS的基本用法:子类重写tryAcquire/tryRelease定义同步语义,外部类委托调用AQS模板方法。

6.2 实现限流器

基于AQS共享模式实现简单的限流器:

java 复制代码
public class SimpleLimiter {
    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int permits) { setState(permits); }
        
        @Override
        protected int tryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 || 
                    compareAndSetState(available, remaining)) {
                    return remaining;
                }
            }
        }
        
        @Override
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }
    }
    
    private final Sync sync;
    
    public SimpleLimiter(int permits) { sync = new Sync(permits); }
    
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
    public void release() {
        sync.releaseShared(1);
    }
}

此限流器类似Semaphore,通过state表示可用许可证数,支持多线程同时获取。

7 AQS进阶特性与最佳实践

7.1 条件变量(Condition)

AQS通过ConditionObject实现条件队列,与同步队列分离。条件变量提供更灵活的线程等待/通知机制。

await()流程

  1. 释放锁,状态保存
  2. 线程加入条件队列
  3. 被signal()唤醒后,重新获取锁

signal()流程

  1. 将条件队列首节点转移到同步队列
  2. 节点在同步队列中等待获取锁

7.2 超时与中断控制

AQS提供acquireInterruptibly()和tryAcquireNanos()方法,支持可中断获取和超时控制。这在避免死锁和实现响应式系统时至关重要。

7.3 性能优化与注意事项

  1. 减少锁竞争:缩小临界区,缩短锁持有时间
  2. 避免死锁:按固定顺序获取多个锁
  3. 状态设计:子类应确保state操作线程安全,通常使用CAS
  4. 避免阻塞操作:tryAcquire()中不应调用可能阻塞的方法
  5. 正确释放:确保获取锁后最终释放,建议try-finally结构

8 总结

AQS作为Java并发编程的基石,通过state状态管理和CLH队列机制,为构建各种同步组件提供了强大而灵活的基础设施。其模板方法设计使开发者能专注于同步语义的实现,而无需关注底层线程排队、阻塞/唤醒等复杂细节。

掌握AQS不仅有助于理解Java并发工具的实现原理,更能为构建高性能、高并发的自定义同步组件奠定基础。在实际开发中,应根据具体场景选择合适的同步模式和策略,平衡性能与公平性需求。

AQS的设计思想,如CAS操作、队列管理、状态机控制等,在分布式系统、数据库连接池等场景也有广泛应用,是每个Java开发者必备的进阶技能。


更多技术干货欢迎关注微信公众号"科威舟的AI笔记"~

【转载须知】:转载请注明原文出处及作者信息

相关推荐
曾富贵2 小时前
【后端进阶】并发竞态与锁选型
后端
a程序小傲2 小时前
京东Java面试被问:ZGC的染色指针如何实现?内存屏障如何处理?
java·后端·python·面试
vx_bisheyuange2 小时前
基于SpringBoot的老年一站式服务平台
java·spring boot·后端·毕业设计
Tony Bai3 小时前
Jepsen 报告震动 Go 社区:NATS JetStream 会丢失已确认写入
开发语言·后端·golang
bing.shao3 小时前
Golang 之 defer 延迟函数
开发语言·后端·golang
penngo3 小时前
Golang使用Fyne开发桌面应用
开发语言·后端·golang
程序员清风3 小时前
别卷模型了!上下文工程才是大模型应用的王道!
java·后端·面试
逸风尊者3 小时前
开发可掌握的知识:uber H3网格
后端·算法
逸风尊者4 小时前
开发需掌握的知识:MQTT协议
java·后端