小册子系列的第二篇文章来啦。虽然上一篇的反响平平,但这打击不了我要坚持下去的决心。废话不多说,上硬菜。
java 并发集大成者,AQS
笔者在这事先说明,本文采用的是 jdk11 的版本进行分析。还是老样子,先把可以回答的点给各位看官列一下:
- AQS 是啥。
- AQS 底册原理。
- AQS 的应用。
脱落的旧时光,看穿了是与非
AQS 是啥
AQS 其实就是 AbstractQueuedSynchronizer 类的简称,它的真面目长这样:
各位读者可能发现我怎么把作者也圈了出来,因为这哥们啊,不简单,他就是被称为 java 并发编程大师的 Doug Lea 老爷子,以一己之力贡献了无数 java 考点的巨佬。AQS 可以说是他的代表作之一,没错,只是之一而已。笔者在开篇就提到了,AQS 是 java 并发的集大成者。因为 AQS 扮演的角色就是一个大管家,管理了跟并发有关的底层实现细节。AQS 大家从它的名称中也可以豹窥一斑,用母语翻译一下就是:抽象的队列同步器。Doug Lea 在注释中也给出了 AQS 的定义,如下:
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic {@code int} value to represent state.
笔者用自己仅存的一些词汇量翻译一下就是:AQS 提供了一种框架用于阻塞锁和相关同步器的实现,是基于 FIFO 的等待队列。AQS 被设计成大多数同步器的基础,并基于一个 int 类型的值来表示状态。笔者根据注释和自己的理解用通俗的话来回答第一个问题就是,AQS 是实现其他同步器的基础,是基于 FIFO 队列实现并使用 int 值来表示自身的同步状态。(这里的 int 值也可以理解成是一种被多个线程同时访问的资源,在源码中就是对应 state 这个变量。)至此,AQS 是什么大家应该也就明白了。
AQS 底册原理
一说到底层原理,可能会有人觉得发怵。但笔者并不打算在这事无巨细的带大家去剖析 AQS 的每一行实现,笔者只会抽出其中的关键方法,分别是 acquire 和 release,这两个可是一对,别只调用其中一个哦。
以独占模式获取,忽略中断。所谓的独占指的就是一个线程拥有了,其他线程只能呆在队列里
从上面的截图中可以看出,acquire 中会额外再调用三个方法,并根据 acquireQueued 方法的返回值来判断是否需要调用 selfInterrupt 方法;这几个方法中最最关键的就是 acquireQueued 和 addWaiter 这两个方法。tryAcquire 方法在 AQS 中并没有提供具体实现,而是留给子类进行实现,子类需要定义如何获取同步资源的逻辑,这也是模板方法设计模式的一种体现。
通过当前线程和给定模式创建队列节点
在上一个问题中,笔者把 FIFO 等待队列 这个词进行了加粗,从上面的代码中大家就能明白为啥了。AQS 把每个线程和对应的模式都封装成一个个的 Node 对象;每一个 Node 对象都会关联一个线程对象、一个属于节点自身的状态值以及对共享资源的获取模式;每一个 Node 对象通过死循环和 cas 操作链接在一起形成一个队列。这个队列是后续所有同步操作的基础。
该方法会出现在每个首次获取资源失败的线程的调用栈中,重点!!
acquireQueued 方法可以划分为以下几部分:
1、当前节点的前驱节点是头节点,则会多一次调用 tryAcquire 方法的机会,如果不是或 tryAcquire 方法返回 false,则转到第二步;如果成功,则表示该节点获取成功,从死循环中跳出
2、修改当前节点的前驱节点的状态值为 -1,前驱节点后续会根据自己的状态值决定要不要唤醒其后面的线程
3、当前驱节点的状态值为 -1 时,则会调用 LockSupport.park(this); 方法将当前线程挂起
进了,就和青春道个别
以独占模式释放,和 acquire 方法成对出现
从上面的截图中可以看出,release 中会额外再调用两个方法,一个是交给子类实现的 tryRelease 方法,子类需要定义如何释放同步资源的逻辑;另一个是 unparkSuccessor 方法。
独占模式,释放操作的关键方法
unparkSuccessor 方法同样可以划分为三部分:
1、判断当前节点的状态是否小于 0,此处需要结合上面提到的 acquireQueued 方法中的第二步进行理解,小于 0,说明你的屁股后面还跟着其他等待资源的线程
2、获取当前节点的后继节点,如果没有后继节点或者后继节点的状态值大于 0,则需要从队列的尾部开始往前找,找到离当前节点最近的,符合条件的后继节点
3、能够找到后继节点,则调用 LockSupport.unpark(s.thread); 方法叫醒它
AQS 的应用
AQS 在平时其实很难会直接看到它的身影,一方面是作为码农的我们大部分时间还是和 crud 打交道,另一方面是自己其实已经用了,但不一定知道。AQS 在 juc 包中都是作为基类被继承的,像用的较多的 Semaphore(信号量)、ReentrantLock(可重入锁)等,其实都是基于 AQS 进行了封装,这些并发工具类对外只是简单的提供了几个 api 供开发使用而已,各位感兴趣的可以看下这些的源码,都能在当中找到 AQS 的身影。
你就像那灯塔,指引着我不惧风浪,向你驶来
本篇文章到这就结束了,AQS 的精华远远不止这些东西,各位看官可以在网上多找些其他资料进行学习。如果有读者对内容持有不同意见的,欢迎在评论区中和笔者进行交流,我们下期再见。