深入剖析Java并发编程中的AQS原理、源码与实际应用
AQS的核心地位与基本概念
AbstractQueuedSynchronizer(AQS)是Java并发编程中一个至关重要的基础框架,它为实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(如信号量、事件等)提供了一个强大的基础设施。Java并发包(java.util.concurrent.locks)中的许多核心工具,如ReentrantLock、Semaphore、CountDownLatch等,其内部实现都重度依赖于AQS。理解AQS的工作机制,是深入掌握Java高并发编程的关键一步。
AQS的底层原理:同步状态与CLH队列
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁(一种虚拟的双向队列)实现的,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个名为state的int类型的volatile成员变量来表示同步状态。通过CAS(Compare-And-Swap)操作来原子性地更新这个状态,从而实现对资源的获取和释放。例如,在ReentrantLock中,state=0表示锁未被任何线程持有,state=1表示锁被一个线程持有,大于1则表示该线程重入了锁。同步器内部依赖一个FIFO的双向队列来完成资源获取线程的排队工作。
AQS的源码结构剖析
AQS的源码结构清晰地划分了其职责。其内部类Node是构成CLH队列的节点,每个等待线程都会被封装成一个Node节点。节点中包含线程引用、等待状态(如CANCELLED、SIGNAL、CONDITION等)以及指向前后节点的指针。
AQS设计采用了模板方法模式,其核心方法可以分为两类:
1. 模板方法: 这些是公开给外部使用的方法,如acquire(int arg)、release(int arg)等。它们定义了获取和释放资源的逻辑骨架,但其内部会调用需要子类实现的钩子方法。
2. 可重写方法: 这些是 protected 方法,需要子类根据具体的同步需求来实现。最重要的是:
tryAcquire(int arg):尝试以独占方式获取资源。tryRelease(int arg):尝试释放独占资源。tryAcquireShared(int arg):尝试以共享方式获取资源。tryReleaseShared(int arg):尝试释放共享资源。isHeldExclusively():判断该线程是否正在独占资源。
这种设计使得开发者无需关心复杂的队列维护和线程阻塞/唤醒机制,只需关注对state状态的判定和更新逻辑即可实现一个自定义的同步器。
从源码看独占式锁的获取与释放:以acquire为例
我们以独占模式下获取资源的入口方法acquire(int arg)为例,深入其源码逻辑:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}该方法遵循一个非常经典且精炼的流程:
第一步: 调用子类实现的tryAcquire(arg)方法尝试直接获取资源。如果成功,则方法直接返回,线程继续执行。
第二步: 如果尝试获取失败,则通过addWaiter(Node.EXCLUSIVE)方法,以独占模式(EXCLUSIVE)将当前线程包装成一个新的Node节点,并采用CAS操作快速地将该节点插入到CLH队列的尾部。
第三步: 调用acquireQueued(final Node node, int arg)方法,让这个节点在队列中自旋(或阻塞)地等待获取资源。在此方法中,节点会不断检查自己的前驱节点是否为头节点(head),如果是,则再次尝试tryAcquire(arg)。如果成功获取资源,则将自己设为新的头节点并返回。如果不是头节点或获取失败,则会根据前驱节点的状态判断是否应该挂起(park)当前线程,以避免不必要的CPU循环。如果线程在等待过程中被中断,acquireQueued方法会返回true。
第四步: 如果acquireQueued返回true,表示线程在等待过程中被中断过,此时调用selfInterrupt()方法补上一个中断标志,但不抛出InterruptedException,这符合锁的不可中断获取语义。
释放资源的过程(release)则相对简单:调用子类的tryRelease成功释放资源后,它会唤醒(unpark)队列中头节点的后继节点,使其有机会尝试获取资源。
AQS的实际应用:构建自定义同步器
通过继承AQS并实现其保护方法,我们可以轻松构建自定义的同步工具。例如,我们可以实现一个简单的二元闭锁(类似CountDownLatch,但不可重置):
public class OneShotLatch { private final Sync sync = new Sync(); public void signal() { sync.releaseShared(0); } public void await() throws InterruptedException { sync.acquireSharedInterruptibly(0); } private class Sync extends AbstractQueuedSynchronizer { @Override protected int tryAcquireShared(int ignored) { // 状态为1表示门闩已开放,允许通过(成功获取);-1表示需要排队等待。 return (getState() == 1) ? 1 : -1; } @Override protected boolean tryReleaseShared(int ignored) { setState(1); // 打开门闩 return true; // 其他等待的线程现在可以成功获取了 } }}在这个例子中,初始状态state为0。调用await()的线程会尝试以共享模式获取资源,tryAcquireShared发现state不为1,返回-1表示失败,线程将被加入队列并阻塞。当某个线程调用signal()时,会调用tryReleaseShared将state置为1,并唤醒队列中所有等待的线程。被唤醒的线程再次尝试tryAcquireShared,此时返回1,获取成功,所有等待线程得以继续执行。这个例子清晰地展示了如何利用AQS的共享模式来实现一个同步工具。
总结
AQS作为Java并发包的基石,其精巧的设计将复杂的同步器实现简化为对同步状态的管理。通过深入理解其基于CLH队列的线程排队机制、状态管理以及模板方法模式的应用,开发者不仅能够更好地使用Java内置的并发工具,还能在遇到特殊业务场景时,有能力构建高效、可靠的自定义同步组件,从而解决复杂的并发控制问题。它是每个Java高级开发者必须掌握的底层核心技术之一。
736

被折叠的 条评论
为什么被折叠?



