啥?你连 AQS 是啥都不知道?
如果想要精通 Java 并发的话, AQS 是一定要掌握的。今天跟着阿粉一起搞一搞
基本概念
AQS 是 AbstractQueuedSynchronizer
的简称,翻译成中文就是 抽象队列同步器
,这三个单词分开来看:
-
Abstract (抽象):也就是说, AQS 是一个抽象类,只实现一些主要的逻辑,有些方法推迟到子类实现
-
Queued (队列):队列有啥特征呢?先进先出( FIFO )对吧?也就是说, AQS 是用先进先出队列来存储数据的
-
Synchronizer (同步):即 AQS 实现同步功能
以上概括一下, AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单而又高效地构造出同步器。
AQS 内部实现
AQS 队列在内部维护了一个 FIFO 的双向链表,如果对数据结构比较熟的话,应该很容易就能想到,在双向链表中,每个节点都有两个指针,分别指向直接前驱节点和直接后继节点。使用双向链表的优点之一,就是从任意一个节点开始都很容易访问它的前驱节点和后继节点。
在 AQS 中,每个 Node 其实就是一个线程封装,当线程在竞争锁失败之后,会封装成 Node 加入到 AQS 队列中;获取锁的线程释放锁之后,会从队列中唤醒一个阻塞的 Node (也就是线程)
AQS 使用 volatile 的变量 state 来作为资源的标识:
1 |
|
关于 state 状态的读取与修改,子类可以通过覆盖 getState() 和 setState() 方法来实现自己的逻辑,其中比较重要的是:
1 |
|
下面是 AQS 中两个重要的成员变量:
1 |
|
关于 AQS 维护的双向链表,在源码中是这样解释的:
1 |
|
也就是 AQS 的等待队列是 “CLH” 锁定队列的变体
直接来一张图会更形象一些:
Node 节点维护的是线程,控制线程的一些操作,具体来看看是 Node 是怎么做的:
1 |
|
AQS 如何获取资源
在 AQS 中,获取资源的入口是 acquire(int arg) 方法,其中 arg 是获取资源的个数,来看下代码:
1 |
|
在获取资源时,会首先调用 tryAcquire 方法,这个方法是在子类中具体实现的
如果通过 tryAcquire 获取资源失败,接下来会通过 addWaiter(Node.EXCLUSIVE) 方法,将这个线程插入到等待队列中,具体代码:
1 |
|
在上面能够看到使用的是 CAS 自旋插入,这是因为在 AQS 中会存在多个线程同时竞争资源的情况,进而一定会出现多个线程同时插入节点的操作,这里使用 CAS 自旋插入是为了保证操作的线程安全性
现在呢,申请 acquire(int arg) 方法,然后通过调用 addWaiter 方法,将一个 Node 插入到了队列尾部。处于等待队列节点是从头结点开始一个一个的去获取资源,获取资源方式如下:
1 |
|
在获取资源时,除了 acquire 之外,还有三个方法:
-
acquireInterruptibly :申请可中断的资源(独占模式)
-
acquireShared :申请共享模式的资源
-
acquireSharedInterruptibly :申请可中断的资源(共享模式)
到这里,关于 AQS 如何获取资源就说的差不多了,接下来看看 AQS 是如何释放资源的
AQS 如何释放资源
释放资源相对于获取资源来说,简单了很多。源码如下:
1 |
|
AQS 两种资源共享模式
资源有两种共享模式:
-
独占模式( Exclusive ):资源是独占的,也就是一次只能被一个线程占有,比如 ReentrantLock
-
共享模式( Share ):同时可以被多个线程获取,具体的资源个数可以通过参数来确定,比如 Semaphore/CountDownLatch
看到这里, AQS 你 get 了嘛?