写点什么

Java 并发工具 AbstractQueuedSynchronizer 实现详解,如何保证高可用

作者:Java高工P7
  • 2021 年 11 月 10 日
  • 本文字数:1567 字

    阅读完需:约 5 分钟

AQS 的实现要点总结如下:


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


1. 用一个原子 int 变量代表同步状态


AQS 内部有一个原子 int 变量(命名为 state),它是 AQS 的核心状态,也是唯一跟同步有关的变量。例如,ReentrantLock 中 state≠0 表示锁已被占,state=0 表示锁空闲。AQS 的子类负责赋予 state 具体含义,通过覆写 tryAcquire(), tryRelease(), tryAcquireShared(), tryReleaseShared(), isHeldExclusively()来通过以乐观锁方式对 state 进行操作。并且,state 只允许通过 getState(), setState(), compareAndSetState()对其操作以保证可见性和原子性。


2. 用一个 CLH 队列存放等待线程,每个线程一个结点,分独占模式和共享模式。


CLH 队列(其实是 CLH 队列的变种)是用于实现自旋锁的队列数据结构,主体是一个链表,如下图所示。



CLH 队列


在 CLH 队列中,每个线程的等待状态(waitState 变量)保存在前一个结点中,取值可以是 1 (CANCELED,线程已经放弃等待),-1(SIGNAL,线程已经被阻塞、需要被唤醒),-2(CONDITION,线程在条件队列中等待,这种状态的结点不可能出现在 CLH 队列,只会出现在 Condition 条件队列上),-3(PROPAGATE,下一个在共享模式中等待的线程无条件唤醒,即唤醒动作可以继续往队列后面接力),0(就绪,这个状态代表线程没有被阻塞,并且准备好请求锁)。


3. 通过循环+CAS 操作来对 CLH 队列进行修改


因为 AQS 本身就是用于实现锁的,所以 AQS 中的 CLH 队列没有锁来保护,且必须支持并发修改。怎么办?通过循环结合 CAS 操作来实现 CLH 队列操作的线程安全性。例如 enq(),


private?Node enq(final?Node node) {?for?(;;) { Node t =?tail;?if?(t ==?null) { // Must initialize?if?(compareAndSetHead(new?Node()))?tail?=?head; }?else?{ node.prev?= t;?if?(compareAndSetTail(t, node)) { t.next?= node;?return?t; } } }}


4. 加锁操作如何实现


与 CLH 队列操作很类似,加锁操作也是通过循环+CAS 操作来实现,不过还使用到了让线程阻塞的方法 LockSupport.park()。我们以不可中断的加锁操作为例,讲解其主要实现逻辑如下:在一个死循环中,首先调用乐观锁加锁操作 tryAcquire(),如果成功,则加锁操作直接返回;如果失败了,则判断当前线程结点的前一结点的 waitState:如果等于 SIGNAL,则直接进入阻塞(说明此时已经有前一个结点得到锁了),如果是 CANCELED,则清理一次 CLH 队列(把已经取消等待的线程中队列中移除)再次执行循环,如果是 0 或 PROPAGATION,则把状态改为 SIGNAL,继续执行循环。如果前面的阻塞操作被其他线程唤醒了,再次执行循环。参考代码:


final boolean acquireQueued(final Node node, int arg) {


boolean failed = true;


try {


boolean interrupted = false;


for (;;) {


final Node p = node.predecessor();


if (p == head && tryAcquire(arg)) {


setHead(node);


p.next = null; // help GC


failed = false;


return interrupted;


}


if (shouldParkAfterFailedAcquire(p, node) &&


parkAndCheckInterrupt())


interrupted = true;


}


} finally {


if (failed)


cancelAcquire(node);


}


}


加锁操作其实有计时版本、可中断版本,但大体逻辑就是上面这样,只不过在循环中再进行了一些时间判断、中断标志判断等,并体现在返回结果或抛出异常上。


总结一下,AQS 的加锁操作就是在一个循环中,不断执行 CAS 加锁(成功则返回,失败则继续),然后不断阻塞和被唤醒(在每次被唤醒的时候,顺便执行一些 CLH 队列清理工作),再次执行 CAS 加锁。


5. 加锁操作优化


AQS 中的加锁操作,即 acquire(),进行了一种 barge 优化。意思就是,当锁被释放出来以后,此时正好有一个线程请求锁,而队列中的第一个线程也被唤醒并且请求锁,这两个线程谁将获得锁是不确定的。这种优化对 AQS 锁的并发性有提升,但是使得它变成不公平的锁(没有按照 FIFO 原则操作)。

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
Java并发工具AbstractQueuedSynchronizer实现详解,如何保证高可用