Java中的AQS原理

136 阅读4分钟

 ``` 本文已参与「新人创作礼」活动,一起开启掘金创作之路。


# AQS 原理

## 1. 概述 

        全称是 AbstractQueuedSynchronizer,是**阻塞式锁和相关的同步器工具的框架。子类继承他来实现同步器。底层实现可参照Monitor理解。主要通过CAS和volitile维护state从而实现的锁。**

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ed9326133ff448d9b469d69dadd7d092~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

**源码展示:**

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c68f91ede0d248a891cbfe09370a0ada~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

**特点:**

         **AQS是个抽象类。AQS及其实现类,类似于Monitor。看下面第二条和第三条。但是Monitor是不可见的,是操作系统提供的。AQS是Java通过CAS和volitile实现的类似于Monitor的同步器。**

-   用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁

-   ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d47414b2a2648768e7d9cdc1f17d59c~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

    -   getState - 获取 state 状态
    -   setState - 设置 state 状态
    -   compareAndSetState - cas 机制设置 state 状态
    -   独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

-   提供了基于 FIFO 的等待队列来实现阻塞队列,类似于 Monitor 的 EntryList。**有一个Node内部类,用来实现FIFO的等待队列 。后续讲ReentrantLock实现原理时,会具体介绍。**

    -   ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/08b7f9b49dec45ee8d9810858dd8cc21~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

-   条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet。使用Lock.newCondition()生成条件变量,底层再用同步器(即AQS的实现类)新建。

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3098029b902d4cf3a06973afc8d2b2c1~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

 **AQS中的条件变量class**

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07fc6c9262fd4ab1be0dd1bc45296b67~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

子类主要实现这样一些方法(默认抛出 UnsupportedOperationException) 

-    tryAcquire
-   tryRelease
-   tryAcquireShared
-   tryReleaseShared
-   isHeldExclusively

获取锁和释放锁。**后续讲ReentrantLock实现原理时,会具体介绍。**

        **获取锁的姿势**

> // 如果获取锁失败\
> if (!tryAcquire(arg)) {\
>         // 入队, 可以选择阻塞当前线程 park unpark\
> }
>
> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57b9f7ddd9844d6f87faafb33a18317a~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​
>
> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a13efb141624470fa848adcbf30bafaf~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

        **释放锁的姿势**

> // 如果释放锁成功\
> if (tryRelease(arg)) {\
>         // 让阻塞线程恢复运行\
> }
>
> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/78c8e18d65e7478f88f0d6ef5737bc52~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​
>
> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/605df0817da64ce3b7baa30e754661d6~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

##  2. 实现不可重入锁

###  自定义同步器

 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/44e5eac83c5a41f88b51fe2a919e53c6~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

final class MySync extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire(int acquires) { if (acquires == 1){ if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } } return false; } @Override protected boolean tryRelease(int acquires) { if(acquires == 1) { if(getState() == 0) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; } return false; } protected Condition newCondition() { return new ConditionObject(); } @Override protected boolean isHeldExclusively() { return getState() == 1; }}


![](<> "点击并拖拽以移动")

### 自定义锁

有了自定义同步器,很容易复用 AQS ,实现一个功能完备的自定义锁。

根据lock方法可以看出同步器sync相当于monitor,有类似于monitor的EntryList的等待队列。

class MyLock implements Lock { static MySync sync = new MySync(); @Override // 尝试,不成功,进入等待队列 public void lock() { sync.acquire(1); //acquire方法是AQS的方法 } @Override // 尝试,不成功,进入等待队列,可打断 public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override // 尝试一次,不成功返回,不进入队列 public boolean tryLock() { return sync.tryAcquire(1); } @Override // 尝试,不成功,进入等待队列,有时限 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); } @Override // 释放锁 public void unlock() { sync.release(1); } @Override // 生成条件变量 public Condition newCondition() { return sync.newCondition(); } }


![](<> "点击并拖拽以移动")

 测试:

MyLock lock = new MyLock(); new Thread(() -> { lock.lock(); //获取成功,进入AQS的实现类MySync同步器成为Owner线程 try { log.debug("locking..."); sleep(1); } finally { log.debug("unlocking..."); lock.unlock(); } },"t1").start(); new Thread(() -> { lock.lock(); //获取失败,MySync创建阻塞队列,将t2线程入队列。 try { log.debug("locking..."); } finally { log.debug("unlocking..."); lock.unlock(); } },"t2").start();


![](<> "点击并拖拽以移动")

**NonfairSync继承自AQS。先简单看一下,后面会详细介绍。**

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2de9e7533fee477a80cce044d239aee8~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

输出:

> 22:29:28.727 c.TestAqs [t1] - locking...\
> 22:29:29.732 c.TestAqs [t1] - unlocking...\
> 22:29:29.732 c.TestAqs [t2] - locking...\
> 22:29:29.732 c.TestAqs [t2] - unlocking...

不可重入测试\
如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)

> lock.lock();\
> log.debug("locking...");\
> lock.lock();\
> log.debug("locking...");

## 3. 心得

### 起源

\
        早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是**在 JSR166(java 规范提案)中创建了 AQS,提供了这种通用的同步器机制。**

### 目标

AQS 要实现的功能目标

-   阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire(尝试失败不进入等待队列)
-   获取锁超时机制
-   通过打断取消机制
-   独占机制及共享机制
-   条件不满足时的等待机制

要实现的性能目标

> Instead, the primary performance goal here is scalability: to predictably maintain efficiency even, or especially, when synchronizers are contended
>
> 相反,这里的主要性能目标是可伸缩性:在争用同步器时,可以预见地保持效率。

### 设计

AQS 的基本思想其实很简单

获取锁的逻辑

> while(state 状态不允许获取) {\
> if(队列中还没有此线程) {\
>         入队并阻塞\
>         }\
> }\
> 当前线程出队
>
> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d256c1e29484405cbede75d401c77f23~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​
>
> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8908533434ab4267920fa49a1fbe9a92~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

释放锁的逻辑

> if(state 状态允许了) {\
>         恢复阻塞的线程(s)\
> }
>
> unpark表示唤醒后面的一个线程。后边会详细介绍。
>
> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54f775911d184d4fa69bd7d7e454dbb9~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

### 要点

\
        ● **原子维护 state 状态(通过CAS和volitile)**\
        ● 阻塞及恢复线程\
        ● 维护队列

**1) state 设计**

-   state 使用 volatile 配合 cas 保证其修改时的原子性。
-   state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想。

**2) 阻塞恢复设计**

-   早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume,那么 suspend 将感知不到
-   解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题
-   park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
-   park 线程还可以通过 interrupt 打断 

**3) 队列设计**

-   使用了 FIFO 先入先出队列,来实现等待队列(类似于Monitor的EntryList)和条件变量。并不支持优先级队列。
-   设计时借鉴了 CLH(同步队列) 队列,它是一种单向无锁队列。
-   关于同步队列,参考这篇博客。[这篇博客https://blog.csdn.net/luzhensmart/article/details/105771974?ops_request_misc=&request_id=&biz_id=&utm_medium=distribute.pc_search_result.none-task-blog-2~all~es_rank~default-10-105771974.pc_search_all_es&utm_term=CLH+%E9%98%9F%E5%88%97&spm=1018.2226.3001.4187![ ](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b7546fc1cc344f59a8e5f9b8d933e099~tplv-k3u1fbpfcp-zoom-1.image)https://blog.csdn.net/luzhensmart/article/details/105771974?ops_request_misc=&request_id=&biz_id=&utm_medium=distribute.pc_search_result.none-task-blog-2~all~es_rank~default-10-105771974.pc_search_all_es&utm_term=CLH+%E9%98%9F%E5%88%97&spm=1018.2226.3001.4187](https://blog.csdn.net/luzhensmart/article/details/105771974?ops_request_misc=&request_id=&biz_id=&utm_medium=distribute.pc_search_result.none-task-blog-2~all~es_rank~default-10-105771974.pc_search_all_es&utm_term=CLH+%E9%98%9F%E5%88%97&spm=1018.2226.3001.4187 "这篇博客https://blog.csdn.net/luzhensmart/article/details/105771974?ops_request_misc=&request_id=&biz_id=&utm_medium=distribute.pc_search_result.none-task-blog-2~all~es_rank~default-10-105771974.pc_search_all_es&utm_term=CLH+%E9%98%9F%E5%88%97&spm=1018.2226.3001.4187")

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b34af9618c0042c0b3ab81e04fc002e6~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

         队列中有 head 和 tail 两个指针节点,**都用 volatile 修饰配合 cas 使用**,每个Node有 state 维护节点状态。

        **入队和出队之后会详细介绍。**\
        入队伪代码,**只需要考虑 tail 赋值的原子性。**

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c9dcd15c75c4f1494648faa1f11283e~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

出队伪代码

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea2c948cb9aa4492ac2019d47daf17e8~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

CLH 好处:

-   无锁,使用自旋
-   快速,无阻塞 

AQS 在一些方面改进了 CLH

private Node enq(final Node node) { for (;;) { Node t = tail; // 队列中还没有元素 tail 为 null if (t == null) { // 将 head 从 null -> dummy if (compareAndSetHead(new Node())) tail = head; } else { // 将 node 的 prev 设置为原来的 tail node.prev = t; // 将 tail 从原来的 tail 设置为 node if (compareAndSetTail(t, node)) { // 原来 tail 的 next 设置为 node t.next = node; return t; } } } }


![](<> "点击并拖拽以移动")

### 主要用到 AQS 的并发工具类

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f906dc1c119e482dac96d8e3e4d6c108~tplv-k3u1fbpfcp-zoom-1.image)![](<> "点击并拖拽以移动")​

​