首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

并发编程系列-AQS

AbstractQueuedSynchronizer(AQS)是一个抽象队列同步器,它用于构建依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器的框架。该类的目的在于提供基本功能的封装,适用于大多数需要使用单个原子int值表示同步状态的同步器。举例来说,ReentrantLock、Semaphore以及FutureTask等都是基于AQS实现的,我们也可以通过继承AQS来实现自定义的同步器。

核心思想

网络上常见的解释是:

如果所请求的共享资源是空闲的,那么当前请求资源的线程将被设置为有效的工作线程,并且共享资源将被锁定。然而,如果所请求的共享资源已被占用,则需要一套机制来阻塞等待线程并在适当时分配锁。AQS采用了CLH队列锁的实现方式来处理这种情况,即将无法立即获取到锁的线程添加到队列中进行排队等待。

我理解,可以把AQS视为一种锁,并将其用于管理共享资源的状态和线程请求。当有线程请求资源时,AQS会记录该线程作为当前工作线程,并将自身设置为锁定状态。如果其他线程请求AQS,则它们将被记录到等待队列中,无法获取到锁的线程进入阻塞等待状态。

为什么需要 AQS

在深入研究AQS之前,我们可能会问为什么需要AQS? 其中synchronized关键字和CAS原子类已经提供了丰富的同步方案。

然而,在实际需求中,我们对同步的需求各不相同。例如,如果我们需要对锁设置超时时间,单独依靠synchronized关键字或CAS是无法实现的,需要进行二次封装。而JDK中提供了丰富的同步方案,比如使用基于AQS实现的ReentrantLock。

用法

要将AQS用作同步器的基础,请根据具体使用情况重新定义以下方法,可以使用getState、setState和/或compareAndSetState来检查和/或修改同步状态:

tryAcquire

tryRelease

tryAcquireShared

tryReleaseShared

isHeldExclusively

这些方法的默认实现会抛出UnsupportedOperationException。这些方法的实现必须是线程安全的,并且通常应该是短暂而不是阻塞的。定义这些方法是使用此类的唯一受支持的方式。所有其他方法都被声明为最终方法,因为它们不应该被修改。

你可能还会发现从AbstractOwnableSynchronizer继承的方法对于跟踪拥有独占同步器的线程非常有用。我们鼓励您使用这些方法,这样监控和诊断工具可以帮助用户确定哪些线程持有锁。

尽管这个类基于内部FIFO队列,但它并不自动执行FIFO调度策略。独占同步的核心形式为:

AQS是一种底层技术,用于实现锁机制。它提供了一种通过队列的方式,将争用锁的线程进行排队等待的机制。与此类似的是,共享模式也是通过类似的机制来实现。然而,共享模式可能涉及到级联信号的问题。

在获取操作之前,新获取的线程有可能会在其他被阻塞和排队的线程之前获得锁资源。如果需要的话,可以通过定义tryAcquire和/或tryAcquireShared方法来禁止插队,从而提供公平的FIFO获取顺序。具体来说,大多数公平同步器可以在tryAcquire返回false时,通过调用hasQueuedPredecessors方法返回true,以保证公平性。还有其他修改的可能性。

默认的插入策略通常是贪婪、放弃和避免护送策略,这种策略可以提供最高的吞吐量和可扩展性。尽管这不能保证公平性或无饥饿,但它允许较早排队的线程在较晚排队的线程之前重新竞争,并且每次重新竞争都有公平机会与传入线程成功竞争。此外,虽然获取操作不是传统意义上的自旋,但它们可能会在阻塞之前多次调用tryAcquire,并在其中插入其他计算。这样做可以提供自旋带来的许多好处,而不会增加太多的负担。如果需要的话,可以通过预先调用具有"快速路径"检查的方法来增加这一点,例如预先检查hasContended和/或hasQueuedThreads,以便仅在同步器可能不会争用的情况下执行自旋操作。

AQS提供了高效且可扩展的同步基础,适用于依赖于整数状态、获取和释放参数以及内部FIFO等待队列的同步器。如果这还不够,可以使用原子类、自定义java.util.Queue类和LockSupport阻塞支持从较低级别构建同步器。

用法示例

这是一个不可重入互斥锁类,它使用值 0 表示未锁定状态,使用值 1 表示锁定状态。虽然不可重入锁并不严格要求记录当前所有者线程,但无论如何,此类都会这样做以使使用情况更易于监控。它还支持条件并公开一些检测方法:

这是一个类似于 CountDownLatch 的锁存器类,只是它只需要一个信号即可触发。因为锁存器是非独占的,所以它使用共享的获取和释放方法。

AQS 底层原理

父类 AbstractOwnableSynchronizer

AbstractQueuedSynchronizer 继承自 AbstractOwnableSynchronizer ,后者逻辑十分简单:

AbstractOwnableSynchronizer 只是定义了设置持有锁的线程的能力。

CLH 队列

AQS(AbstractQueuedSynchronizer)的等待队列是CLH(Craig, Landin, and Hagersten)锁定队列的一种变体。CLH锁通常用于自旋锁。AQS通过将每个请求共享资源的线程封装为一个CLH节点来实现。这个节点的定义如下:

CLH节点的数据结构是一个双向链表的节点,其中每个操作都经过CAS(Compare and Swap)来确保线程安全。要将其加入CLH锁队列,您可以将其自动拼接为新的尾节点;要出队,则需要设置头节点,以便下一个满足条件的等待节点成为新的头节点:

Node 中的 status 字段表示当前节点代表的线程的状态。status 存在三种状态:

WAITING:表示等待状态,值为 1。

CANCELLED:表示当前线程被取消,为 0x80000000。

COND:表示当前节点在等待条件,也就是在条件等待队列中,值为 2。

在上面的 COND 中,提到了一个条件等待队列的概念。首先,Node 是一个静态抽象类,它在 AQS 中存在三种实现类:

ExclusiveNode

SharedNode

ConditionNode

前两者都是空实现:

而最后的 ConditionNode 多了些内容:

ConditionNode 拓展了两个方法:

检查线程状态是否处于等待。

阻塞当前线程:当前线程正在等待执行,通过 阻塞当前线程。这里通过 while 循环持续重试,尝试阻塞线程。

而到这一步,所有的信息都指向了一个相关的类 Condition 。

总结

AQS是锁机制实现的底层技术,它通过队列的方式将争用锁的线程进行排队等待。与此不同的是,Condition代替了Object中的同步方法能力。

当Condition进入等待状态时,它会在等待队列的队尾插入新的节点,并且通过release方法释放锁资源。在释放锁资源后,它会通过acquire开启一个自旋尝试重新获取锁资源。当自旋一定时间后,如果未成功获取到锁资源,就会通过LockSupport的API进入阻塞状态。

对于依赖单个原子int值来表示同步状态的解读,Node通过status属性来控制与之关联的线程的等待状态,而AQS的state用于控制同步状态。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/ODK1Et3sdfqADawwjhIua14w0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券