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

简介: 本文深入解析Java并发核心组件AQS(AbstractQueuedSynchronizer),从其设计动机、核心思想到源码实现,系统阐述了AQS如何通过state状态、CLH队列和模板方法模式构建通用同步框架,并结合独占与共享模式分析典型应用,最后通过自定义锁的实战案例,帮助读者掌握其原理与最佳实践。
  • 引言
  • 一、为什么需要AQS?
  • 二、AQS核心思想:一套通用的并发框架
  • 三、核心组件解析
  • 四、两种模式:独占与共享
  • 五、源码流程浅析
  • 六、实战:用AQS实现一个简单的锁
  • 七、总结与最佳实践
  • 互动环节

引言

商业创意

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

答案就藏在
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. 对于ReentrantLockstate表示锁被重入的次数(0表示空闲,>0表示被占用)。
  3. 对于Semaphorestate表示剩余的许可证数量
  4. 对于CountDownLatchstate表示还需要倒计数的数量
  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)

多个线程可以同时成功获取资源,如SemaphoreCountDownLatch

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

五、源码流程浅析

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

// 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

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锁

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的acquirerelease模板方法帮我们完成了。

七、总结与最佳实践

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

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


相关文章
|
前端开发 Java C++
JUC系列之《CompletableFuture:Java异步编程的终极武器》
本文深入解析Java 8引入的CompletableFuture,对比传统Future的局限,详解其非阻塞回调、链式编排、多任务组合及异常处理等核心功能,结合实战示例展示异步编程的最佳实践,助你构建高效、响应式的Java应用。
|
1月前
|
缓存 安全 Java
JUC系列《深入浅出Java并发容器:CopyOnWriteArrayList全解析》
CopyOnWriteArrayList是Java中基于“写时复制”实现的线程安全List,读操作无锁、性能高,适合读多写少场景,如配置管理、事件监听器等,但频繁写入时因复制开销大需谨慎使用。
|
7月前
|
消息中间件 算法 安全
JUC并发—1.Java集合包底层源码剖析
本文主要对JDK中的集合包源码进行了剖析。
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
存储 消息中间件 安全
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
【10月更文挑战第9天】本文介绍了如何利用JUC组件实现Java服务与硬件通过MQTT的同步通信(RRPC)。通过模拟MQTT通信流程,使用`LinkedBlockingQueue`作为消息队列,详细讲解了消息发送、接收及响应的同步处理机制,包括任务超时处理和内存泄漏的预防措施。文中还提供了具体的类设计和方法实现,帮助理解同步通信的内部工作原理。
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
1130 6
【Java学习】多线程&JUC万字超详解
|
存储 并行计算 算法
深入解析Java并发库(JUC)中的Phaser:原理、应用与源码分析
深入解析Java并发库(JUC)中的Phaser:原理、应用与源码分析
|
存储 缓存 Java
深入剖析Java并发库(JUC)之StampedLock的应用与原理
深入剖析Java并发库(JUC)之StampedLock的应用与原理
深入剖析Java并发库(JUC)之StampedLock的应用与原理
|
存储 安全 Java
Java多线程编程--JUC
Java多线程编程
163 2