引言
如果问一个 Android 同学,请你简单说一下 Java AQS 的基本思想,那么有不少于一半的同学可能是
什么玩意,AQS 是什么,我咋没听过🙃。
的确,对于非Java后端同学来说,没听过倒也不是什么太过分的事,但是如果你深入学习过 Java 并发相关,那么肯定会去了解各种锁,而作为一个 有志青年 的你必然会在心里来一句,为什么加了锁就可以同步 ? 此时必然也会看到 AQS
的影子。
从技术的角度讲,当我们谈到 ReentrantLock
,不难也会说到 AQS
。
如果上述所讲你不了解的话,那么本篇文章可能会对你有所帮助。
背景
在了解一项技术之前,我们有必要了解这项技术所产生的原因,及它能为我们带来什么?
AQS 全名 AbstractQueuedSynchronizer,在 jdk1.5 时加入 ,是整个 JUC 的基础,我们现在用到的大多数同步器都是基于 AQS 的,其性能相比于 常用的 synchronized 在一定程度上提高了不少,而它的构建者 也就是并发包的大师 (Doug Lea) 更是期望它能够成为实现大部分同步需求的基础。
那AQS出现原因是什么呢? 难道 AQS 的出现仅仅是为了提升性能吗,或者说仅仅是因为性能,就要重复造一个 AQS 的轮子?
要解释这个问题,首先我们要思考一下在它出现之前,我们经常使用的同步方式是 synchronized ,而 synchronized 是没办法解决死锁,因为其在申请资源时,如果申请不到,线程将直接进入阻塞状态,也即释放不了线程已经占用的资源,而为了解决这个问题,我们就需要新的方案。
如果让我们自己重写去设计一个工具来解决上述问题,那该怎么去设计,AQS提供了以下方案?
支持响应中断。 synchronized 一旦进入阻塞状态,就无法被中断。但如果阻塞状态的线程能够响应中断信号,能够被唤醒。这样就破坏了不可被抢占条件了。
支持超时。 如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁,这样也能破坏不可抢占条件。
非阻塞的获取锁。 如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
AQS是什么?
抽象队列同步器 AbstractQueuedSynchronizer ,是用来众多同步组件的一个 基础框架 , 内部用到了 CLH 队列锁 。
如果之前没了解过,那么这句话可能听着有点懵,先别急着搞清原理,我们先在脑海里有个概念 :
这玩意是用来构建锁的基础框架 , 这玩意是用来构建锁的基础框架 , 这玩意是用来构建锁的基础框架
有人说,AQS 只是一个工具类而已,的确如此。但是其的重要性在并发编程中用 框架 这个词或许更为合适。
1. AQS 基础理论入门
AQS 其主要使用方式是继承,即子类通过继承 AQS 实现它的抽象方法来管理状态,内部使用一个 int 成员变量 state 表示同步状态,并且通过一个 FIFO(先进先出) 的队列来完成线程的排队工作。
在具体的实现上,通常子类推荐被定义为静态内部类(就像 ReentrantLock中 的 Sync ),AQS 本身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态,这也是 jdk 中内置的同步组件的实现原理。
总览
描述一下工作流程:
每次在请求一个资源时,先将其添加到 CLH 队列中,并且 state+1 , 队列处于循环遍历阶段(当 state=0 时,代表资源无占用),队列循环过程中,如果被请求的共享资源空闲状态,则将当前请求资源的线程设置为有效的工作线程,并且更改其node节点中的 locked 为 true ,代表其处于占用状态,后续结点(即后续需要获取资源的线程)就一直处于自旋等待状态,不停的判断前一个节点 的 locaked 是否为 false ,如果为 false ,则证明此时资源处于空闲状态,则去尝试获取资源。
模板方法模式
如果翻阅 ReentrantLock 的源码,会发现一个问题,那就是 除了最开始的 lock() 是 ReentrantLock 本身提供的方法之外,而剩下的方法解析,也就是是其内部的核心方法调用,都是调用了 AQS 的相关方法。
并且都 NonfairSync 实现了 AQS 的一些方法,比如 tryAcquire() ,以便于提供具体的实现逻辑。
而这样的设计我们称之为 模板方法模式 :
即也可以总结为:对于一个设计而言,我们将不容易改变的方法提前定义好具体的实现逻辑,并且将一些需要具体实现的交给外部自己实现。并将这种类似的方法按照我们的逻辑需要,包装成几个通用的方法调用合体,以供外部调用。
- 对于外部而言,其本身并不知道我内部的具体实现;
- 外部需要实现自己相应的独特逻辑的几个特定方法;
这样的好处就是 我们封装了不变的部分,仅扩展了可变部分,并且提取了公共代码,以便于维护。
CLH队列锁
CLH队列锁 是一种基于链表的可扩展,高性能,公平的自旋锁,申请资源的线程仅仅在本地变量上自旋,它不断轮训前驱节点的状态,假设发现前驱释放了锁,就结束自旋。
当一个线程需要获取锁时,先创建一个的 QNode ,将其中的 locked 设置 true 表示需要获取锁, myPred 表示对其前驱结点的引用。
其完整流程如下所示:
- 假设线程A要获取资源,其先使自己成为队列的尾部,同时获取一个指向其前驱结点的引用
myPred
,并不断在父节点引用上自旋判断。
2.当另一个线程B同样也需要获取锁时,上述的过程同样也要来一遍,如下所示 (QNode-B):
3.当某个线程要释放锁时,就将当前节点的 locked
设置为 false
。
其后续节点因为不断在自旋,当判断到其前序节点 locked
为 false
,就表明其前序节点已经释放锁,其自身就可以获取到锁,并且释放当前前序节点引用,以便GC回收。
整个过程如上图所示,CLH队列锁 的优点是空间复杂度低,如果有n个线程,L个锁,每个线程每次都只获取一个锁,那么其需要的存储空间 O(L+n) ,n个线程有n个 node,L 个锁有L个tail .
AQS 中的 CLH队列锁 实现方式与上述方式相比是一种变体的实现,相比普通 CLH队列锁 ,AQS 中的实现方式 做了相关的优化,比如不会不断重试,而会在重试相关次数后将线程阻塞。等待之后的唤醒。
AQS 相关方法
模板方法
在我们实现自定义的同步组件时,将会调用同步器提供的模板方法。相关方法如下:
上述模板方法同步器提供的模板方法分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放
- 同步状态和查询同步队列中的等待线程情况。
可重写的方法
访问或修改同步状态的方法
在自定义的同步组件框架中,AQS 抽象方法在实现过程中免不了要对同步状态 state 进行更改,这时就需要同步器提供的3个方法来进行操作,因为他们能够保证状态的改变是安全的:
getState()
获取当前同步状态
setState(newState:Int)
设置当前同步状态
compareAndSetState(expect:Int,update:Int)
使用 CAS 设置当前状态,该方法能保证状态设置的原子性。