java aqs优点_JAVA并发编程之AQS(1)— AQS论文分析总结

什么是AQS

全称 AbstractQueuedSynchronizer,它是一个框架,为同步状态的原子性管理、线程的阻塞和唤醒以及队列模型提供一种通用机制。

JAVA中的同步器(ReentrantLock,CountDownLatch,Semphore等等)都基于他所构建的

为什么要学

理解各类同步器是怎么实现的,理解并发

理解这个框架的设计思路和方法,可以学到一些抽象的思维

变的更强

基本功能

AQS定义了一个同步器至少包含两种方法

acquire:阻塞线程,直到同步状态允许其继续执行

release:释放线程,通过某种方式改变同步状态,使得一或多个被Acquire的线程继续执行

j.u.c包中并没有对同步器的API做一个统一的定义。有一些类定义了通用的接口(如Lock),而另外一些则定义了其专有的版本。因此在不同的类中,以acquire和release操作的名字和形式会各有不同。

例如:Lock.lock,Semaphore.acquire,CountDownLatch.await和FutureTask.get,在这个框架里,这些方法都是acquire操作

基于约定,每一个同步器还要实现以下的功能

阻塞和非阻塞的尝试(例如tryLock)

可选的超时设置,让调用者可以放弃等待

通过中断实现的任务取消

为了使框架能得到广泛应用,要支持以下两种模式的同步器

独占式 exclusive:在同一时间只有一个线程可以通过阻塞点

共享式 shared:允许多个线程通过阻塞点

例如ReentrantLock是使用独占式模式实现的,而CountDownLatch用的是共享式。

设计

同步器背后的基本思想非常简单。

acquire操作伪代码如下:

while (synchronization state does not allow acquire) {

enqueue current thread if not already queued;

possibly block current thread;

}

dequeue current thread if it was queued;

复制代码

翻译如下:

while (当同步状态不允许获取的时候) {

if(该线程没有入队){

入队

}

阻塞当前线程(可能)

}

将当前线程出队(如果入队)

复制代码

release操作位伪代码如下:

update synchronization state;

if (state may permit a blocked thread to acquire)

unblock one or more queued threads;

复制代码

翻译如下:

更新线程的同步状态

if(状态允许一个阻塞的线程去获取){

释放一个或者多个在入列的线程

}

复制代码

为了实现上面的acquire和release操作,需要下面这三个组件相互协作

同步状态的原子性管理

线程的阻塞与解除阻塞

队列的管理

创建一个框架分别实现这三个组件是有可能的。但是,这会让整个框架既难用又没效率。例如:存储在队列节点的信息必须与解除阻塞所需要的信息一致,而暴露出的方法的签名必须依赖于同步状态的特性。

所以AQS的核心其实是为以上三个组件提供一个具体的实现

下面我们来聊一下这个具体的实现

实现

同步状态

AQS类使用单个int(32位)来保存同步状态,并暴露出getState、setState以及compareAndSet操作来读取和更新这个状态。这些方法都依赖于j.u.c.atomic包的支持。

这个包提供了volatile在读和写上的语义,并且通过使用本地的compare-and-swap或load-linked/store-conditional指令来实现compareAndSetState,使得仅当同步状态拥有一个期望值的时候,才会被原子地设置成新值。这个也就是我们常说的CAS操作

基于AQS的具体实现类必须根据暴露出的状态相关的方法定义tryAcquire和tryRelease方法,以控制acquire和release操作。

当同步状态满足时,tryAcquire方法必须返回true

而当新的同步状态允许后续acquire时,tryRelease方法也必须返回true。

这些方法都接受一个int类型的参数用于传递想要的状态。

这个参数主要用来实现不同子类功能的,例如ReentrantLock使用该参数去操作线程的同步状态实现了重入的计数

阻塞

AQS没有采用Thread.suspend和Thread.resume这两种方式,以上两种方式都有严重的安全问题,例如容易造成死锁等。

AQS采用了j.u.c包下的LockSupport类。该类可以响应中断操作,可以设置超时时间等。

队列

整个框架的关键就是如何管理被阻塞的线程的队列,该队列是严格的FIFO队列,因此,框架不支持基于优先级的同步。

AQS的锁策略采用的CLH而不是MCS,原因是CLH要比MCS更适合处理取消和超时。

因此我们选择了CLH锁作为实现的基础。但是最终的设计已经与原来的CLH锁有较大的出入。

这里简单介绍一下CLH

CLH队列实际上并不那么像队列,因为它的入队和出队操作都与它的用途(即用作锁)紧密相关。它是一个链表队列,通过两个字段head和tail来存取,这两个字段是可原子更新的,两者在初始化时都指向了一个空节点。f28a53c389b21054f8d56ebb3bac577f.png

一个新的节点,node,通过一个原子操作入队:

do {

pred = tail;

} while(!tail.compareAndSet(pred, node));

复制代码

每一个节点的“释放”状态都保存在其前驱节点中。因此,自旋锁的“自旋”操作就如下:

while (pred.status != RELEASED); // spin

复制代码

自旋后的出队操作只需将head字段指向刚刚得到锁的节点:

head = node;

复制代码

使用CLH锁有以下优点

入队和出队操作是快速、无锁的,以及无障碍的(即使在竞争下,某个线程总会赢得一次插入机会而能继续执行)

判断是否有线程正在等待也很快(测试一下head是否与tail相等)

“释放”状态是分散的(几乎每个节点都保存了这个状态,当前节点保存了其前驱节点的“释放”状态,因此它们是分散的,不是集中于一块的),避免了一些不必要的内存竞争。

为了将CLH队列用于阻塞式同步器,AQS做出了以下改进:

给每一个节点增加next域

在自旋锁中,一个节点只需要改变其状态,下一次自旋中其后继节点就能注意到这个改变,所以节点间的链接并不是必须的

但在阻塞式同步器中,一个节点需要显式地唤醒(unpark)其后继节点

所以AQS增加了节点node访问其后继节点的next域

由于AQS队列是双向队列,所以CAS操作也没有很好的方式对两个方向都做到完全的原子性更新。后继结点的更新就采用了下面的简单赋值

pred.next = node;

复制代码next链接仅是一种优化。如果通过某个节点的next字段发现其后继结点不存在(或看似被取消了),总是可以使用pred字段从尾部开始向前遍历来检查是否真的有后续节点

每个节点都有自己的状态字段用于控制阻塞而非自旋

论文这里作者用了很大的篇幅去写节点状态位的东西,我简单的归纳成两个问题:

一个released状态位够不够?

如果不够,还要哪些?加这些状态位有什么好处?

问题1解答:只有一个released位是不够的,AQS还需要当一个活动线程在头结点时候仅调用tryAcquire

在同步器框架中,仅在线程调用具体子类中的tryAcquire方法返回true时,队列中的线程才能从acquire操作中返回

单个“released”位是不够的,还需要确保一个活动的线程仅在队列的头部,调用tryAcquire方法,这时的acquire可能会失败,然后(重新)阻塞

这时候不需要一个前驱的状态去判断是否阻塞,直接可以判断这个前驱的节点是不是头部,不像自旋锁需要内存复制的竞争

但是取消状态还是要读前驱的状态

这个节点的状态还可以避免不必要的park和unpark,虽然这些方法跟阻塞原语一样快,但在跨越Java和JVM以及操作系统边界时仍有可避免的开销。

在调用park前,线程设置一个“唤醒(signal)”位,然后再一次检查同步和节点状态。一个释放的线程会清空其自身状态,这样线程就不必频繁地尝试阻塞。

依赖JVM回收节点内存,这就避免了一些复杂性和开销

AQS主要使用在出队的时候置null方式回收节点内存,这可以有效的避免复杂的处理和瓶颈。

抛开这些细节,基本的acquire操作的最终实现的一般形式如下

if(!tryAcquire(arg)) {

node = create and enqueue new node;

pred = node's effective predecessor;

while (pred is not head node || !tryAcquire(arg)) {

if (pred's signal bit is set)

park()

else

compareAndSet pred's signal bit to true;

pred = node's effective predecessor;

}

head = node;

}

复制代码

翻译如下

if (!tryAcquire(arg)) {

node = 创建队列并且新入队节点;

pred = 节点的有效前驱节点;

while (pred 不是头节点 || !tryAcquire(arg)) {

if (pred的状态位是Signal信号)

park();

else

CAS操作设置pred的Signal信号;

pred = node节点的有效前驱节点;

}

head = node;

}

复制代码

release的操作如下

if(tryRelease(arg) && head node's signal bit is set) {

compareAndSet head's bit to false;

unpark head's successor, if one exist

}

复制代码

翻译如下

release {

if (tryRelease(arg) && 头节点的状态是Signal) {

将头节点的状态设置为不是Signal;

如果头节点的后继结点存在,则将其唤醒。

}

}

复制代码

acquire操作的主循环次数依赖于具体实现类中tryAcquire的实现方式。

另一方面,在没有“取消”操作的情况下,每一个组件的acquire和release都是一个O(1)的操作(忽略park中发生的所有操作系统线程调度)

本文参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值