首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JUC系列之《深入理解AQS:Java并发锁的基石与灵魂 》

JUC系列之《深入理解AQS:Java并发锁的基石与灵魂 》

原创
作者头像
用户2364152
发布2025-10-14 11:36:46
发布2025-10-14 11:36:46
1800
代码可运行
举报
运行总次数:0
代码可运行

简介: 本文深入解析Java并发核心组件AQS(AbstractQueuedSynchronizer),从其设计动机、核心思想到源码实现,系统阐述了AQS如何通过state状态、CLH队列和模板方法模式构建通用同步框架,并结合独占与共享模式分析典型应用,最后通过自定义锁的实战案例,帮助读者掌握其原理与最佳实践。

  • 引言
  • 一、为什么需要AQS?
  • 二、AQS核心思想:一套通用的并发框架
  • 三、核心组件解析
  • 四、两种模式:独占与共享
  • 五、源码流程浅析
  • 六、实战:用AQS实现一个简单的锁
  • 七、总结与最佳实践
  • 互动环节

引言

商业创意

在Java并发编程中,ReentrantLock、Semaphore、CountDownLatch等强大的同步工具类为我们解决了各种各样的线程协作难题。你是否曾好奇,这些功能各异的工具类,其底层是如何实现的?

答案就藏在 java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)这个看似晦涩的抽象类中。AQS是整个JUC包同步器的基石和灵魂,它用一个优雅的框架,封装了构建锁和同步器的核心细节。理解AQS,就如同获得了打开Java并发世界宝库的钥匙,让你能从“使用者”进阶为“理解者”和“创造者”。


一、为什么需要AQS?

在AQS出现之前,如果要实现一个自定义的锁或同步器,开发者需要直面复杂的线程排队、阻塞、唤醒等底层操作。这些操作不仅容易出错,而且难以优化。

AQS的设计目标:提供一个通用的、模板化的框架,让开发者可以专注于实现同步状态的获取与释放逻辑(即“什么是同步条件”),而将复杂的线程排队、等待、唤醒等机制(即“如何管理排队线程”)交给AQS底层统一实现。

简单来说:AQS负责管理“排队”,你(同步器的实现者)只负责定义“什么时候放人进去”。

二、AQS核心思想:一套通用的并发框架

AQS的核心思想可以用一个经典的银行办事大厅的比喻来理解:

  1. 状态(State):好比大厅里的空闲柜台数量。int state是AQS的一个volatile变量,是同步状态的核心。不同的同步器对state的含义有不同的解释:
  2. 对于ReentrantLock,state表示锁被重入的次数(0表示空闲,>0表示被占用)。
  3. 对于Semaphore,state表示剩余的许可证数量
  4. 对于CountDownLatch,state表示还需要倒计数的数量
  5. CLH队列:好比大厅里的排队等候区。AQS内部维护了一个FIFO的双向队列(一个变种的CLH队列),所有暂时无法获取到同步状态的线程都会被封装成Node节点,加入到这个队列中排队等待。
  6. 模板方法模式:AQS定义了顶级流程骨架(如acquire获取资源、release释放资源),但将一些关键步骤(如tryAcquire尝试获取资源)设计为protected方法,交由子类去实现。这就是“你定规则,我管排队” 的协作方式。

三、核心组件解析

1. 同步状态:state

这是一个volatile int类型的变量,是所有操作的核心。AQS提供了三种原子方法来操作它:

  • getState(): 获取当前状态。
  • setState(int newState): 设置状态。
  • compareAndSetState(int expect, int update): 使用CAS操作原子性地更新状态,保证线程安全。

2. 节点(Node)与同步队列

无法立即获取锁的线程会被包装成一个Node节点。每个Node包含了:

  • 代表的线程(Thread thread)
  • 等待状态(int waitStatus),如CANCELLED(已取消)、SIGNAL(需要唤醒后继节点)等。
  • 前驱指针(Node prev)和后继指针(Node next)

这些Node节点共同组成了一个双向的FIFO队列,即同步队列。AQS通过这个队列来管理所有等待线程的排队和唤醒。

四、两种模式:独占与共享

AQS支持两种工作模式,这决定了资源是如何被线程获取的。

1. 独占模式(Exclusive)

一次只有一个线程能成功获取资源,如ReentrantLock。

  • 核心方法
  • acquire(int arg): 获取资源(模板方法,不可中断)。
  • release(int arg): 释放资源(模板方法)。
  • tryAcquire(int arg): 需要子类实现。尝试获取资源,成功返回true,失败返回false。
  • tryRelease(int arg): 需要子类实现。尝试释放资源。

2. 共享模式(Shared)

多个线程可以同时成功获取资源,如Semaphore、CountDownLatch。

  • 核心方法
  • acquireShared(int arg): 获取资源。
  • releaseShared(int arg): 释放资源。
  • tryAcquireShared(int arg): 需要子类实现。尝试获取资源。返回负数为失败;0表示成功,但后续共享获取可能不会成功;正数表示成功,且后续共享获取可能成功。
  • tryReleaseShared(int arg): 需要子类实现。尝试释放资源。

五、源码流程浅析

以独占模式的acquire方法为例,看看AQS是如何工作的:

代码语言:javascript
代码运行次数:0
运行
复制
// AQS中的模板方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) && // 步骤1:子类尝试获取一次资源
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 步骤2&3:获取失败,则加入队列并等待
        selfInterrupt(); // 如果是中断模式,则补上中断标记
}
  1. tryAcquire(arg):首先调用子类实现的tryAcquire方法尝试获取一次锁。如果成功,整个流程结束。
  2. addWaiter(Node.EXCLUSIVE):如果上一步尝试失败,则将当前线程包装成一个独占模式的Node节点,并通过CAS操作快速添加到同步队列的尾部。
  3. acquireQueued(node, arg):这是核心中的核心。让已经入队的节点,以“自旋(循环尝试)”的方式不断尝试获取资源:
  4. 检查自己的前驱节点是不是头节点(head)。如果是,说明自己是排队的第一个,则再次调用tryAcquire尝试获取资源。
  5. 如果获取成功,将自己设为新的头节点,并脱离队列。
  6. 如果前驱不是头节点,或者尝试再次失败,则可能会挂起(park) 当前线程,等待被前驱节点唤醒。
  7. 被唤醒后,继续循环检查自己是否是头节点的后继,并尝试获取资源。

release流程相对简单

  1. 调用子类实现的tryRelease(arg)尝试释放资源。
  2. 如果释放成功(state变为0),则唤醒同步队列中头节点的后继节点(即下一个等待的线程)。

六、实战:用AQS实现一个简单的锁

理解了原理,让我们动手实现一个最简单的(非重入)互斥锁Mutex。

代码语言:javascript
代码运行次数:0
运行
复制
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
/**
 * 一个简单的不可重入互斥锁
 */
public class Mutex implements Lock {
    // 静态内部类,继承AQS,实现具体的同步逻辑
    private static class Sync extends AbstractQueuedSynchronizer {
        
        // 是否处于占用状态(state为1表示占用,0表示空闲)
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        
        // 尝试获取锁(AQS模板方法acquire会调用此方法)
        @Override
        protected boolean tryAcquire(int acquires) {
            // 断言: acquires 必须为 1
            assert acquires == 1;
            // 使用CAS操作,尝试将state从0改为1
            if (compareAndSetState(0, 1)) {
                // 成功!设置当前线程为独占所有者
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            // CAS失败,获取锁失败
            return false;
        }
        
        // 尝试释放锁(AQS模板方法release会调用此方法)
        @Override
        protected boolean tryRelease(int releases) {
            // 断言: releases 必须为 1
            assert releases == 1;
            // 如果状态已经是0,说明锁已经是空闲的,释放操作异常
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            // 释放锁,将独占所有者清空
            setExclusiveOwnerThread(null);
            // 注意:state的volatile写放在最后,保证之前的修改对获取锁的线程可见
            setState(0);
            return true;
        }
    }
    // 将具体工作代理给Sync对象
    private final Sync sync = new Sync();
    @Override
    public void lock() {
        sync.acquire(1); // 调用AQS的模板方法,AQS会去调用我们重写的tryAcquire
    }
    @Override
    public void unlock() {
        sync.release(1); // 调用AQS的模板方法,AQS会去调用我们重写的tryRelease
    }
    // 其他Lock接口方法(略实现)
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    @Override
    public boolean tryLock(long time, java.util.concurrent.TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
    @Override
    public java.util.concurrent.Condition newCondition() {
        // 简单实现,不支持Condition
        throw new UnsupportedOperationException();
    }
}

使用这个自定义的Mutex锁

代码语言:javascript
代码运行次数:0
运行
复制
public class MutexExample {
    private static final Mutex lock = new Mutex();
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            lock.lock(); // 获取我们自定义的锁
            try {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            } finally {
                lock.unlock(); // 释放锁
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final count: " + count); // 正确输出 20000
    }
}

通过这个简单的例子,你可以清晰地看到AQS是如何工作的:我们只定义了“获取锁的条件是CAS(0,1)成功”,而线程排队、阻塞、唤醒等复杂机制全部由AQS的acquire和release模板方法帮我们完成了。

七、总结与最佳实践

  1. AQS的地位:AQS是JUC同步组件的基石,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等都是基于它构建的。
  2. 核心机制:通过一个volatile int state表示资源状态,一个FIFO队列管理排队线程, combined with CAS操作模板方法模式,构建了一套高效、通用的同步框架。
  3. 设计模式:AQS是模板方法模式的经典应用。父类定义算法骨架,子类实现可变细节。
  4. 最佳实践
  5. 作为使用者:理解AQS能让你更深刻地理解各种JUC同步工具类的原理和特性,从而更好地使用它们。
  6. 作为开发者:除非有极其特殊的同步需求,否则应优先直接使用JUC包提供的现成同步器,而不是自己基于AQS造轮子。它们已经经过千锤百炼,是高效且稳定的。
  7. 若要自定义:如果需要实现一个全新的、现有同步器无法满足需求的同步原语,继承AQS并重写tryAcquire、tryRelease等方法是最佳选择。

AQS是Java并发大师Doug Lea的作品,其设计之精妙,堪称艺术品。理解它,不仅是为了应对面试,更是为了提升我们对并发编程本质的认识,培养设计复杂系统的架构能力。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 一、为什么需要AQS?
  • 二、AQS核心思想:一套通用的并发框架
  • 三、核心组件解析
  • 1. 同步状态:state
  • 2. 节点(Node)与同步队列
  • 四、两种模式:独占与共享
  • 1. 独占模式(Exclusive)
  • 2. 共享模式(Shared)
  • 五、源码流程浅析
  • 六、实战:用AQS实现一个简单的锁
  • 七、总结与最佳实践
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档