AQS为什么使用双向FIFO队列

924 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

壁纸.jpeg

前言

AQS(AbstractQueuedSynchronizer)是java.util.concurrent包下一个非常核心的类,我们经常使用的ReentrantLock、CountDownLatch,都是基于抽象同步式队列实现的。

AQS作为一个抽象类,通常是通过继承来使用的。它本身是没有同步接口的,只是定义了同步状态和同步获取和同步释放的方法。

JUC包下面的所有同步类可以说,都是基于AQS的同步状态的获取与释放来实现的,同时AQS也是个链表结构的双向队列。

下面我们来看下,为什么AQS是双向队列而不是单向的呢?

源码分析

0B760FFA-58B3-451F-8A26-AE51947DB17A.png

首先通过查看JUC包下的AbstractQueuedSynchronizer源码,我们可以看到有这么一个图,箭头都是从尾节点指向头节点的,乍一看这不就是单向队列。

A52D94C3-4CBE-42C4-B1D2-78E3206ED452.png

在往下看有个内部类Node,Node有个前驱节点和后继节点,就是说既可以指向前面又可以指向后面,每个Node都是由线程封装,在争抢锁失败后会封装成Node加入AQS队列。

正常的CLH单向列表结构,对于线程的争抢锁,入队、出队、与释放是完全够用的。 但是遇到中断或者唤起阻塞的线程,释放锁移除节点的时候,就需要用到prev和next。

在移除节点的时候,如意采用CLH单向列表那么将要轮询整个链表,因为只维护了一个向前的指针,不从tail去遍历是无法获取到后一个节点的指针的。在多线程竞争的环境下,轮询是非常耗性能的,因此采用prev和next来降低操作的复杂性。

中断、删除、唤醒节点

C1E7C9D8-450A-4A59-A737-0C6C86880D61.png

主要看cancelAcquire这个方法,删除中断线程节点,并且从AQS队列中删除。主要是通过waitStatus这个volatile修饰的可见属性,来将可能发生修改的的Node复制到,当前线程栈中。 Node.CANCELLED 标记当前线程节点为取消。

v2-952eb5d56a4d8e6e57b5b96c6d5b2277_r.jpg

FF035BEB-608B-4BD1-A0D2-F9250B8E02D9.png

判断前一个节点是不是head节点,如果不是,保证前一个节点的状态是SIGNAL,通过CAS将前一个节点指向尾部节点,以此在链表上删除自己。 当前节点的前一个节点如果为头节点,通过unparkSuccessor该方法进行尾部遍历,找到一个正常的节点进行唤醒。

总结

由此看来,AQS使用双向链表队列结构,主要是为了解决获取锁的过程中的中断,以及用于活跃线程释放锁后主动唤醒后续阻塞线程去竞争锁。