持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
前言
AQS(AbstractQueuedSynchronizer)是java.util.concurrent包下一个非常核心的类,我们经常使用的ReentrantLock、CountDownLatch,都是基于抽象同步式队列实现的。
AQS作为一个抽象类,通常是通过继承来使用的。它本身是没有同步接口的,只是定义了同步状态和同步获取和同步释放的方法。
JUC包下面的所有同步类可以说,都是基于AQS的同步状态的获取与释放来实现的,同时AQS也是个链表结构的双向队列。
下面我们来看下,为什么AQS是双向队列而不是单向的呢?
源码分析
首先通过查看JUC包下的AbstractQueuedSynchronizer源码,我们可以看到有这么一个图,箭头都是从尾节点指向头节点的,乍一看这不就是单向队列。
在往下看有个内部类Node,Node有个前驱节点和后继节点,就是说既可以指向前面又可以指向后面,每个Node都是由线程封装,在争抢锁失败后会封装成Node加入AQS队列。
正常的CLH单向列表结构,对于线程的争抢锁,入队、出队、与释放是完全够用的。 但是遇到中断或者唤起阻塞的线程,释放锁移除节点的时候,就需要用到prev和next。
在移除节点的时候,如意采用CLH单向列表那么将要轮询整个链表,因为只维护了一个向前的指针,不从tail去遍历是无法获取到后一个节点的指针的。在多线程竞争的环境下,轮询是非常耗性能的,因此采用prev和next来降低操作的复杂性。
中断、删除、唤醒节点
主要看cancelAcquire这个方法,删除中断线程节点,并且从AQS队列中删除。主要是通过waitStatus这个volatile修饰的可见属性,来将可能发生修改的的Node复制到,当前线程栈中。 Node.CANCELLED 标记当前线程节点为取消。
判断前一个节点是不是head节点,如果不是,保证前一个节点的状态是SIGNAL,通过CAS将前一个节点指向尾部节点,以此在链表上删除自己。 当前节点的前一个节点如果为头节点,通过unparkSuccessor该方法进行尾部遍历,找到一个正常的节点进行唤醒。
总结
由此看来,AQS使用双向链表队列结构,主要是为了解决获取锁的过程中的中断,以及用于活跃线程释放锁后主动唤醒后续阻塞线程去竞争锁。