Java并发编程之队列同步器AQS(AbstractQueuedSynchronizer)

1. 是什么

你想了解的AQS(AbstractQueuedSynchronizer)是Java并发包(java.util.concurrent)的核心基础组件,是由Doug Lea设计的一个抽象队列同步器,本质是一个用于构建锁(Lock)和同步器(Semaphore、CountDownLatch等)的框架。

其核心内涵:

  • 原子方式管理同步状态(通过volatile int state变量);
  • 内置FIFO双向等待队列(CLH队列变体)管理等待获取资源的线程;
  • 采用模板方法模式,将同步器的通用逻辑(如队列管理、线程阻塞/唤醒)封装,仅暴露核心逻辑(如获取/释放资源)由子类实现。

关键特征:

  • 独占式/共享式:支持独占(如ReentrantLock)和共享(如CountDownLatch)两种资源获取模式;
  • 可重入:支持线程重入获取资源(通过记录当前持有线程和重入次数);
  • 阻塞与唤醒:基于LockSupport的park/unpark实现线程的安全阻塞和唤醒;
  • 非阻塞CAS:核心操作(如状态修改、队列节点入队)依赖CAS保证原子性。

2. 为什么需要

在AQS出现前,你若要实现并发同步,只能依赖synchronized关键字,但它存在明显痛点,而AQS恰好解决了这些问题:

核心痛点

  1. synchronized是JVM层面的隐式锁,灵活性差:无法手动控制锁的获取/释放时机、不支持超时获取、不可中断;
  2. 自定义同步器成本高:若要实现如“计数信号量”“闭锁”等同步工具,需从零实现线程排队、阻塞、唤醒等逻辑,易出错;
  3. 资源管理混乱:缺乏统一的资源(锁)状态管理机制,不同同步工具的实现逻辑无法复用。

实际应用价值

  • 统一基础框架:JUC包中几乎所有同步工具(ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等)都基于AQS实现,避免重复造轮子;
  • 高度可定制:你只需重写AQS的几个核心方法(如tryAcquiretryRelease),即可快速实现自定义锁/同步器;
  • 高性能:基于CAS+自旋+阻塞的混合策略,兼顾非阻塞的高效性和阻塞的资源节约性,性能优于早期synchronized(JDK1.6后synchronized优化后差距缩小,但灵活性仍不如AQS)。

3. 核心工作模式

AQS的核心运作逻辑围绕“状态管理+队列等待”展开,关键要素及关联如下:

关键要素

要素 作用
同步状态(state) volatile int类型,原子存储资源的可用数量/锁的持有状态(如0=未锁定,1=已锁定)
CLH等待队列 双向FIFO队列,存储因获取资源失败而阻塞的线程,每个节点对应一个等待线程
独占/共享模式 独占:同一时间仅一个线程获取资源(如ReentrantLock);共享:多个线程可同时获取(如Semaphore)
条件队列(Condition) 可选的单向队列,用于线程等待/唤醒(如ReentrantLock的Condition)
CAS操作 保证state修改、队列节点操作的原子性,避免加锁

核心机制

  1. 状态原子操作:通过getState()/setState()/compareAndSetState()操作state,其中compareAndSetState是CAS核心,保证多线程下状态修改的原子性;
  2. 队列管理机制
    • 线程获取资源失败时,封装为Node节点加入队列尾部(CAS入队);
    • 队列头节点是“已获取资源”的线程,后续节点是等待线程;
    • 头节点释放资源时,唤醒后继节点尝试获取资源;
  3. 模板方法+钩子方法
    • 模板方法:AQS提供acquire(独占获取)、release(独占释放)、acquireShared(共享获取)等通用逻辑;
    • 钩子方法:子类需重写tryAcquire(独占获取尝试)、tryRelease(独占释放尝试)、tryAcquireShared(共享获取尝试)等方法,实现具体的资源获取/释放逻辑。

4. 工作流程

独占式获取/释放资源(如ReentrantLock)为例,结合流程图拆解完整链路:

核心流程图(Mermaid)

graph TD A["线程调用acquire()获取资源"] --> B{"调用tryAcquire()尝试获取"} B -- 成功 --> C["获取资源,流程结束"] B -- 失败 --> D["封装为Node节点,CAS加入队列尾部"] D --> E["自旋检查前驱节点是否为头节点"] E -- 是 --> F["再次尝试tryAcquire()获取资源"] F -- 成功 --> G["成为新头节点,流程结束"] F -- 失败 --> H["通过LockSupport.park()阻塞当前线程"] E -- 否 --> H I["其他线程调用release()释放资源"] --> J{"调用tryRelease()尝试释放"} J -- 失败 --> K["释放失败,流程结束"] J -- 成功 --> L["唤醒队列后继节点线程"] L --> M["被唤醒的线程回到步骤E,重新自旋尝试获取资源"] M --> E

分步拆解

(1)独占式获取资源(acquire())

  1. 线程调用acquire(int arg)(如ReentrantLock的lock()底层调用);
  2. AQS先调用子类实现的tryAcquire(arg)
    • 成功:直接获取资源,流程结束;
    • 失败:进入队列等待逻辑;
  3. 将当前线程封装为Node.EXCLUSIVE(独占)节点,通过CAS原子性加入队列尾部;
  4. 线程进入自旋(循环):
    • 检查前驱节点是否为头节点(说明自己是下一个该获取资源的线程);
    • 若是,再次调用tryAcquire(arg)尝试获取资源;
    • 若成功:将当前节点设为头节点,流程结束;
    • 若失败:通过LockSupport.park()阻塞当前线程,等待被唤醒;
  5. 线程被唤醒后,重复自旋逻辑,直到获取资源或中断。

(2)独占式释放资源(release())

  1. 线程调用release(int arg)(如ReentrantLock的unlock()底层调用);
  2. AQS调用子类实现的tryRelease(arg)
    • 失败:返回false,释放流程结束;
    • 成功:继续下一步;
  3. 获取队列头节点,通过LockSupport.unpark()唤醒头节点的后继节点线程;
  4. 被唤醒的线程回到自旋逻辑,重新尝试获取资源。

(3)共享式流程差异

共享式(如Semaphore)的核心差异在tryAcquireShared(arg)

  • 返回值≥0:表示获取资源成功;
  • 返回值<0:表示获取失败,加入队列等待;
  • 释放资源时,唤醒的后继节点会继续唤醒其后续节点(因为共享模式允许多个线程同时获取资源)。

5. 入门实操

下面带你实现一个简易的独占式不可重入锁,基于AQS入门实操,让你快速掌握核心用法:

前置条件

  • JDK8及以上;
  • 熟悉基本的Java多线程知识(线程创建、启动);
  • 了解CAS和volatile的基本概念。

实操步骤

步骤1:自定义同步器(继承AQS)

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

// 自定义独占式同步器,实现不可重入锁的核心逻辑
class SimpleExclusiveSync extends AbstractQueuedSynchronizer {
    // 1. 尝试获取资源(独占式)
    @Override
    protected boolean tryAcquire(int arg) {
        // CAS修改state:0→1表示获取锁成功
        if (compareAndSetState(0, 1)) {
            // 设置当前线程为独占锁持有者
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        // 不可重入:即使是当前持有线程,再次获取也返回false
        return false;
    }

    // 2. 尝试释放资源(独占式)
    @Override
    protected boolean tryRelease(int arg) {
        // 检查释放线程是否为持有线程
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new IllegalMonitorStateException("非锁持有者无法释放锁");
        }
        // 释放锁:state重置为0
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    // 3. 判断是否处于独占模式(AQS模板方法需要)
    @Override
    protected boolean isHeldExclusively() {
        return getState() == 1;
    }

    // 对外暴露锁的获取/释放方法
    public void lock() {
        acquire(1); // 调用AQS的模板方法acquire
    }

    public void unlock() {
        release(1); // 调用AQS的模板方法release
    }
}

步骤2:封装为锁工具类

// 自定义不可重入锁
public class SimpleExclusiveLock {
    // 内部持有自定义同步器
    private final SimpleExclusiveSync sync = new SimpleExclusiveSync();

    // 获取锁
    public void lock() {
        sync.lock();
    }

    // 释放锁
    public void unlock() {
        sync.unlock();
    }
}

步骤3:测试锁的功能

public class AQSDemo {
    private static int count = 0;
    private static final SimpleExclusiveLock lock = new SimpleExclusiveLock();

    // 累加任务
    private static void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
            System.out.println(Thread.currentThread().getName() + ":count = " + count);
            Thread.sleep(100); // 模拟业务耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock(); // 释放锁(必须在finally中,避免死锁)
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建10个线程,每个线程执行1次累加
        for (int i = 0; i < 10; i++) {
            new Thread(AQSDemo::increment, "线程" + i).start();
        }
        // 等待所有线程执行完毕
        Thread.sleep(2000);
        System.out.println("最终count值:" + count); // 预期输出10
    }
}

关键操作要点

  1. 钩子方法重写规则
    • 独占模式:必须重写tryAcquiretryRelease,可选重写isHeldExclusively
    • 共享模式:必须重写tryAcquireSharedtryReleaseShared
    • 不要重写AQS的模板方法(如acquirerelease),仅重写钩子方法;
  2. 资源释放必须在finally中:避免线程异常导致锁无法释放,引发死锁;
  3. CAS操作的参数compareAndSetState的预期值和新值需根据业务逻辑设定(如不可重入锁用0→1,可重入锁需累加state)。

实操注意事项

  1. 不可重入锁的局限性:若线程在持有锁的情况下再次调用lock(),会导致自己阻塞(死锁),如需重入,需修改tryAcquire逻辑(判断当前线程是否为持有者,若是则累加state);
  2. 未处理中断:上述示例中acquire()是不可中断的,若需支持中断,可使用acquireInterruptibly()
  3. 无超时机制:可使用tryAcquireNanos()实现超时获取锁,避免线程永久阻塞。

6. 常见问题及解决方案

问题1:AQS实现的锁出现死锁

现象

线程阻塞后无法被唤醒,程序卡死,jstack查看线程状态为WAITING (parking)

原因

  1. 释放锁时未在finally中执行unlock(),导致线程异常退出后锁未释放;
  2. 重入锁的tryRelease逻辑错误(如state未正确递减,导致tryRelease返回false,无法唤醒后继节点);
  3. 自定义同步器的tryAcquire逻辑错误,导致线程重复入队且无法获取资源。

解决方案

  1. 强制释放锁在finally中:所有lock()后必须在finally块中调用unlock(),示例:
    lock.lock();
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
    
  2. 正确实现可重入锁的释放逻辑
    @Override
    protected boolean tryRelease(int arg) {
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new IllegalMonitorStateException();
        }
        int newState = getState() - arg;
        boolean free = (newState == 0);
        if (free) {
            setExclusiveOwnerThread(null);
        }
        setState(newState); // 先修改state,再释放持有线程
        return free;
    }
    
  3. 通过jstack排查死锁:执行jps获取进程ID,再执行jstack <pid>,查看OWNABLE_SYNCHRONIZERS部分,定位持有锁的线程和等待锁的线程。

问题2:共享模式下线程唤醒不彻底

现象

Semaphore等共享同步器中,释放资源后仅部分等待线程被唤醒,其余线程仍阻塞。

原因

共享模式下tryReleaseShared返回true后,AQS仅唤醒后继节点,但后继节点获取资源后未继续唤醒其后续节点(共享模式需“传播”唤醒)。

解决方案

  1. 正确实现tryReleaseShared:返回true表示需要唤醒后继节点;
  2. 确保共享释放的传播性:AQS的releaseShared会自动处理传播唤醒,只需保证tryReleaseShared逻辑正确,示例:
    @Override
    protected int tryAcquireShared(int arg) {
        // 共享获取:state >= arg时成功,否则失败
        for (;;) {
            int current = getState();
            int newCount = current - arg;
            if (newCount < 0 || compareAndSetState(current, newCount)) {
                return newCount; // <0失败,≥0成功
            }
        }
    }
    
    @Override
    protected boolean tryReleaseShared(int arg) {
        // 共享释放:CAS累加state
        for (;;) {
            int current = getState();
            int newCount = current + arg;
            if (compareAndSetState(current, newCount)) {
                return true; // 返回true触发唤醒
            }
        }
    }
    

问题3:AQS同步器性能低下

现象

高并发下,基于AQS的同步器(如ReentrantLock)出现大量线程自旋、上下文切换,CPU使用率高但吞吐量低。

原因

  1. 自旋次数过多:AQS默认的自旋逻辑在高竞争下会导致线程空转,消耗CPU;
  2. 阻塞/唤醒频繁:大量线程进入等待队列,频繁的park/unpark导致上下文切换;
  3. 同步器选择不当:独占锁用于高并发读场景(应使用共享锁或读写锁)。

解决方案

  1. 使用带超时的获取方法:避免线程无限自旋/阻塞,示例:
    // 尝试获取锁,500ms超时
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    } else {
        // 超时处理,避免自旋
        return;
    }
    
  2. 选择合适的同步模式:读多写少场景使用ReentrantReadWriteLock(读共享、写独占),减少锁竞争;
  3. 优化自旋策略:自定义AQS时,可在acquire前增加有限次数的自旋(如3次),减少入队概率:
    public void lock() {
        // 先自旋3次尝试获取锁,失败再走AQS的acquire
        int spinCount = 0;
        while (spinCount < 3 && !tryAcquire(1)) {
            spinCount++;
            Thread.yield(); // 让步,减少CPU消耗
        }
        if (!tryAcquire(1)) {
            acquire(1);
        }
    }
    

总结

  1. 核心定位:AQS是JUC并发工具的基础框架,通过state管理资源状态、CLH队列管理等待线程,采用模板方法模式支持自定义同步器;
  2. 核心逻辑:线程获取资源时先尝试CAS修改state,失败则入队自旋/阻塞;释放资源时修改state并唤醒队列后继线程,独占/共享模式的核心差异在资源获取逻辑;
  3. 实操关键:重写AQS的钩子方法(tryAcquire/tryRelease等),释放锁必须在finally中,根据场景选择独占/共享模式,避免死锁和性能问题。
posted @ 2026-02-25 16:24  先弓  阅读(0)  评论(0)    收藏  举报