AQS原理案例

wen java案例 1

本文目录导读:

AQS原理案例

  1. 目录导读
  2. AQS是什么?为什么它是JUC的基石
  3. 核心原理:state变量 + CLH变体队列的协同机制
  4. 源码级拆解:acquire与release的完整流程
  5. 案例实战:基于AQS手写一个不可重入锁
  6. 常见问题问答(FAQ)
  7. AQS设计哲学与性能边界

目录导读

  1. AQS是什么?为什么它是JUC的基石
  2. 核心原理:state变量 + CLH变体队列的协同机制
  3. 源码级拆解:acquire与release的完整流程
  4. 案例实战:基于AQS手写一个不可重入锁
  5. 常见问题问答(FAQ)
  6. AQS设计哲学与性能边界

AQS是什么?为什么它是JUC的基石

AQS(AbstractQueuedSynchronizer)是Java并发包(java.util.concurrent)中所有锁和同步器的基础抽象类,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等核心工具,内部均继承或组合了AQS,它的本质是一个提供“状态位+等待队列”的同步框架,让开发者只需重写几个钩子方法,就能实现自定义同步器。

核心三要素

  • volatile int state:同步状态,例如锁的持有次数、信号量剩余许可数。
  • CLH变体双向队列:存储竞争失败的线程节点(Node),通过自旋+park/unpark管理阻塞与唤醒。
  • 模板方法模式acquire()release()为模板,tryAcquire()tryRelease()为钩子,子类只需定义“如何改变状态”即可。

核心原理:state变量 + CLH变体队列的协同机制

CLH锁在原始设计中是单向链表,AQS将其改造为双向链表 + 尾插法,并增加状态标记(CANCELLED=1、SIGNAL=-1、CONDITION=-2、PROPAGATE=-3),关键改进点:

  • 避免惊群:每个节点只唤醒其后继节点,而非广播唤醒所有线程。
  • 支持中断与超时:通过节点状态和LockSupport.parkNanos实现。
  • 公平/非公平策略:非公平锁在tryAcquire中直接CAS抢state,不公平但吞吐量高;公平锁则检查队列中是否有前驱节点。

入队与唤醒的微观过程

  1. 线程A持有锁,state=1。
  2. 线程B尝试获取失败,封装为Node,CAS尾插队列,并设置前驱节点为SIGNAL。
  3. 线程B调用LockSupport.park()挂起。
  4. 线程A释放锁(state=0),唤醒队列头部后继节点(线程B)。
  5. 线程B醒来后,再次CAS尝试获取锁,并移除自己的节点。

源码级拆解:acquire与release的完整流程

// AQS.acquire() 模板
public final void acquire(int arg) {
    if (!tryAcquire(arg)) {           // 钩子:子类实现
        Node node = addWaiter(Node.EXCLUSIVE); // 入队
        acquireQueued(node, arg);     // 循环自旋/park
    }
}

关键点分析

  • addWaiter:快速CAS尾插,失败则用enq()自旋保证入队成功。
  • acquireQueued:循环内先检查前驱是否为head,若是则再次尝试tryAcquire;否则应设置前驱SIGNAL后park。
  • 中断处理:若线程被中断,仅在退出时补偿selfInterrupt(),保证不丢失中断状态。

释放流程

public final boolean release(int arg) {
    if (tryRelease(arg)) {           // 钩子:将state减为0
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);      // 唤醒后继
        return true;
    }
    return false;
}

案例实战:基于AQS手写一个不可重入锁

public class Mutex implements Lock {
    private final Sync sync = new Sync();
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            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;
        }
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }
    @Override public void lock() { sync.acquire(1); }
    @Override public void unlock() { sync.release(1); }
    // 其他接口方法略...
}

测试验证:启动10个线程,每个线程对共享变量做1000次累加,使用上述锁后,最终结果必为10000,证明互斥性正确。


常见问题问答(FAQ)

Q1:AQS为什么用双向链表?单向不行吗?
A:双向链表便于高效取消节点(将前驱后继直接连接)和扫描队列(如处理超时或中断),单向链表这些操作需O(n)复杂度。

Q2:非公平锁性能一定优于公平锁吗?
A:不一定,非公平锁减少线程唤醒切换,吞吐量更高;但在高竞争下可能造成饥饿,公平锁则牺牲部分性能换取公平性,实际选择取决于业务场景(如交易系统需公平,秒杀系统可非公平)。

Q3:state变量为什么必须volatile?
A:因为多线程CAS操作依赖volatile保证可见性,否则一个线程修改后,其他线程可能看不到最新状态,导致锁失效。

Q4:CLH队列中的Park和Unpark开销大吗?
A:与自旋锁相比,park/unpark会涉及内核态切换,性能开销较大,但能避免CPU空转,JDK后续加入了AbstractQueuedLongSynchronizer及轻量级锁优化,但AQS仍是核心框架。


AQS设计哲学与性能边界

AQS的精华在于将“阻塞-唤醒”与“状态管理”解耦,通过模板模式定制化扩展,它实现了三个关键平衡:

  • 空间换时间:队列节点缓存线程引用,减少线程重建。
  • 公平与吞吐的权衡:通过钩子让子类自行决策。
  • 中断/超时机制的透明集成:统一在acquireQueued中处理。

性能边界:AQS在低竞争(1-4线程)下表现优秀(CAS几乎成功);高竞争下由于线程频繁阻塞唤醒,性能下降明显,此时可考虑LongAdderPhaser等更细粒度工具。

理解AQS不仅是应对面试的“八股文”,更是进阶高并发编程的分水岭——读透这份源码,你会对Java并发模型的底层心跳有更加深刻的感知。

抱歉,评论功能暂时关闭!