Java高级面试必问:AQS 到底是什么?

Java高级面试必问:AQS详解与应用

本文详细介绍了Java并发编程中的核心组件AQS(AbstractQueuedSynchronizer),包括其基本概念、工作原理以及应用场景。

在现代多线程开发中,同步和互斥机制是确保程序正确性和高效性的关键。而在Java平台下,AbstractQueuedSynchronizer(简称AQS) 是一种构建锁和其他同步器的基础框架,提供了强大的并发控制能力,能够实现种类繁多的锁和同步机制。本文将带领读者全面了解AQS的工作原理及其在实际开发中的应用。

一、AQS是什么?

AQS(AbstractQueuedSynchronizer)是一个用于构建高级线程同步机制的类库框架。它为开发者提供了构建自定义锁及同步组件的能力,使得我们不仅能实现标准的互斥锁和读写锁,还能创建出具有特殊功能的并发工具。

二、为什么需要AQS?

1. 问题背景

在Java早期版本中,synchronized关键字是唯一的线程同步机制。然而它存在一些明显的局限性:

  • 无法实现公平锁:只能提供非公平的获取策略。
  • 缺少超时等待功能:没有支持等待特定时间后放弃锁的功能。
  • 缺乏条件变量的支持:无法通过精确控制来唤醒特定线程或组合多个条件表达式。
  • 不能中断等待中的线程:一旦进入同步状态,只能通过正常执行流程退出。

2. AQS的解决方案

AQS提供了一个灵活且高效的并发框架。它允许开发者:

  • 定义多种自定义锁策略;
  • 实现更复杂的同步需求;
  • 获得更好的性能优化。

三、AQS的工作原理与架构设计

核心结构:CLH队列 + 状态变量

CLH(Craig, Landin 和 Hagersten)队列 是一种自旋链表,用于维护获取资源的线程顺序。每个节点代表一个等待或持有锁状态线程。

public abstract class AbstractQueuedSynchronizer {
    private volatile int state; // 状态变量
    private transient volatile Node head; // 队首节点
    private transient volatile Node tail; // 队尾节点

    static final class Node { // 节点类定义
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        volatile int waitStatus;
    }
}

两种同步模式:独占模式与共享模式

独占模式(Exclusive Mode)

  • 特点:在一个时刻,仅允许一个线程获取资源。
  • 典型应用:ReentrantLock、CountDownLatch
  • 核心方法
    • acquire(int arg):尝试获取锁,若无法立即获得,则将当前线程加入等待队列并触发自旋操作。
    • release(int arg):释放已经持有的锁,并唤醒下一个节点中的等待线程。
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

private Node addWaiter(Node mode) { // 添加到队尾的新节点
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node); // 原子添加操作
    return node;
}

共享模式(Shared Mode)

  • 特点:允许多个线程同时获取资源,如读锁或信号量。
  • 典型应用:Semaphore、ReentrantReadWriteLock
  • 核心方法
    • acquireShared(int arg):尝试获取共享资源。
    • releaseShared(int arg):释放持有的共享资源,并可能唤醒多个后继节点。
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) { // 检查是否队列中有等待的线程
            int ws = h.waitStatus; // 获取头结点状态值
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 修改节点的状态为PROPAGATE,用于传播唤醒信号
                    continue;
                unparkSuccessor(h); // 唤醒下一个等待的线程
            }
            else if (ws == 0 && compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                while (!compareAndSetWaitStatus(head = h, Node.PROPAGATE, 0)) { } // 继续唤醒后面的节点
        }
    }
}

四、实际开发中如何与AQS交互

场景1:使用现成的AQS实现

在大多数情况下,我们无需直接操作AbstractQueuedSynchronizer类。Java并发库提供了多个基于AQS构建的标准工具类。

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;

public class AQSInPractice {
    public static void main(String[] args) throws InterruptedException {
        // 使用ReentrantLock实现可重入锁,参数true表示启用公平模式
        ReentrantLock lock = new ReentrantLock(true);

        System.out.println("锁已获取");
        lock.unlock();

        // 使用CountDownLatch协调多个线程的执行顺序
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    latch.countDown(); // 每次调用减少计数器值
                    System.out.println("任务" + i + "完成");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }

        latch.await(); // 等待直到所有线程都执行完毕

        System.out.println("所有任务完成");

        // 使用Semaphore实现信号量控制
    }
}

通过这些标准的并发工具,可以轻松实现常见的同步需求。希望本文能帮助读者对AQS有一个全面而深入的理解,并在实际开发中灵活运用其强大功能。

下一节我们将继续讨论如何利用AQS自定义锁以及更多高级场景的应用,敬请期待!

场景2:基于AQS实现自定义同步器

可以利用AbstractQueuedSynchronizer (AQS)作为基础,通过继承它来构建满足特定需求的锁机制。以下是一个简单的示例代码,用于创建一个“最多允许N个线程访问”的自定义锁:

public class SimpleLimitLock {
    private final Sync sync;

    public SimpleLimitLock(int limit) {
        sync = new Sync(limit);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int limit) { 
            setState(limit); // 将初始状态设置为允许的最大线程数
        }

        protected int tryAcquireShared(int acquires) {
            int available = getState();
            int remaining = available - acquires;

            if (remaining < 0 || compareAndSetState(available, remaining)) { 
                return remaining; // 如果剩余许可不足,返回负值;如果CAS成功,则返回剩余许可数
            }
            return -1; // 表示尝试获取失败  
        }

        protected boolean tryReleaseShared(int releases) {
            int current = getState();
            int next = current + releases;

            if (compareAndSetState(current, next)) { 
                return true; // CAS更新状态成功
            }
            return false; // 返回CAS操作的状态结果
        }
    }

    public void lock() { sync.acquireShared(1); }    
    public void unlock() { sync.releaseShared(1); }
}

这段代码展示了如何通过AQS来控制线程访问资源的数量,确保并发时的线程安全。此类自定义同步器适用于需要精细控制访问权限的应用场景。

五、AQS在Java并发体系中的角色

AbstractQueuedSynchronizer (AQS) 在 Java 并发编程中扮演着至关重要的角色。它提供了构建高级同步对象的基础结构,如锁和信号量等。下图展示了 Java 并发编程的层次结构:

// Java并发编程的层次结构:
┌─────────────────────────────────────────────────┐
│             Java并发应用层                        │
│  • 线程池 (ExecutorService)                     │
│  • 并发集合 (ConcurrentHashMap)                 │
│  • 异步任务 (CompletableFuture)                 │
├─────────────────────────────────────────────────┤
│             AQS工具类层                          │
│  • ReentrantLock                                │
│  • Semaphore                                    │
│  • CountDownLatch                               │
│  • CyclicBarrier                                │
├─────────────────────────────────────────────────┤
│             AQS框架层                            │
│  • 状态管理 (state)                             │
│  • 队列管理 (CLH队列)                           │
│  • 线程阻塞/唤醒 (LockSupport)                  │
├─────────────────────────────────────────────────┤
│             JVM/OS层                            │
│  • synchronized监视器锁                         │
│  • CAS指令 (Unsafe类)                           │
│  • 线程调度 (操作系统)                          │
└─────────────────────────────────────────────────┘

六、AQS的工作流程示例

以 ReentrantLock 的加锁过程为例,其工作原理主要依赖于 AQS 提供的模板方法和内部状态管理机制。具体步骤如下:

sync.acquire(1); // 调用AQS的模板方法
abstract static class Sync extends AbstractQueuedSynchronizer {
    // AQS.acquire() 的调用链:
    // 1. tryAcquire() 尝试获取锁(子类实现)
    // 2. addWaiter() 创建节点加入队列
    // 3. acquireQueued() 在队列中自旋/阻塞
    // 4. 如果被中断过,自我中断恢复状态
}

整个流程确保了线程安全及资源的正确释放。例如,在尝试获取锁失败的情况下,线程会被放置在等待队列中直至获得锁;若在此期间发生中断请求,则会适当处理并恢复相关状态。

七、实际开发中的最佳实践

1. 选择合适的同步工具

根据具体场景的需求来挑选最适用的并发控制工具。例如:

public class SyncToolSelection {
    void useReentrantLock() { 
        ReentrantLock lock = new ReentrantLock(true); // 公平锁,适用于需要公平获取锁的应用程序
    }

    void useSemaphore() { 
        Semaphore semaphore = new Semaphore(10); // 最多允许10个线程同时访问
        // 适用于数据库连接池、限流场景等资源受限的环境
    }

    void useCountDownLatch() {
        CountDownLatch latch = new CountDownLatch(5);
        // 适用于启动阶段等待初始化完成,确保所有部件就绪后进入工作状态 
    }

    void useCyclicBarrier() { 
        CyclicBarrier barrier = new CyclicBarrier(4); 
        // 用于并行计算场景中,多个线程需同步到达某个固定点
    }   
}

2. 避免常见陷阱

在使用AQS相关类时需要注意以下问题:

public class AQSPitfalls {
    ReentrantLock lock = new ReentrantLock();

    void conditionVariableMistake() { 
        Condition condition = lock.newCondition(); // 错误示例:没有在lock保护下调用await

        try {
            lock.lock();

            if (someCondition) {
                return; // 忘记解锁!
            }
            lock.unlock();

            condition.await(); // 正确的调用方式,必须在持有锁的状态下进行
        } catch (InterruptedException e) { 
            Thread.currentThread().interrupt(); 
        }
    }
}

八、总结:AQS在并发编程中的角色

  1. 框架提供者 :为构建高级同步对象(如锁和信号量)提供了通用的实现框架。
  2. 基础设施 :作为Java并发包的核心机制,如同建造大楼的地基一样重要。
  3. 性能优化 :相比synchronized关键字提供了更灵活且高效的线程控制方式。
  4. 功能扩展 :支持各种高级特性,如公平/非公平模式、可重入性以及读写锁分离等。

> 🔗 相关阅读并发编程