在理解AQS 之前有必要先理解一下锁的部分概念:
-
重入锁 、不可重入锁:线程拿到锁之后,是否可以自由(多次)进入同一把锁同步的其他代码;
-
独占锁、共享锁:获得这种锁的线程是否可修改资源,可以暂时简单地将独占理解为写,共享理解读。
AQS 全称为AbstractQueuedSynchronizer ,是JDK 提供的一个同步工具类,官方将 AQS描述为提供了一个基于FIFO(First in First out)实现了阻塞锁和同步器框架。
以下截图为JDK 1.8中的类注释,另外本文也是基于JDK 1.8 进行探讨。
概念上来说AQS 是比较抽象的东西,我自己会将其概括理解为一种对锁操作的封装:把线程封装为节点对象,FIFO 队列作为锁池的承载对象,根据一定的规则来进行线程与锁的调度。
下面将由重入锁为入口来帮助理解一下AQS 。
重入锁:
重入的概念已经简单的介绍了,那么要做到线程可重入 我们可能需要三个东西,一个用于记录锁的持有者(owner),一个用于记录重入次数的计数值(count),一个保存线程的等待队列(waiter);有了这三样东西那么简单的逻辑我们就可以理出来:
获取锁过程: 有线程进入抢锁,
先判断count是否为0,如果为0
就使用CAS操作将count进行+1操作,然后修改owner的值,抢占锁成功;
如果count不为0,判断owner是否为当前线程
如果owner为当前线程,进行重入,
即将count进行+1操作
否则抢锁失败,进入等待队列。
事实上java 也是这么做的,附上JDK中ReentrantLock的代码:
解锁过程:先判断线程的是否为当前线程,如果不是当前线程 抛出异常解锁失败;否则对count进行-1操作,当count == 0 时 将owner设置为null,返回成功;否则返回失败,如果count不为0就意味着未完全解锁,这样别的线程是也拿不到锁的。
代码截图:
这是对于一个非公平锁的重入锁的实现,如果是公平锁我们只需要多判断一步 抢锁线程是否为等待队列中的线程就可以了,hasQueuedPredecessors()就实现了这么一个功能。
接着我们来看看重独享锁和共享锁是怎么依靠AQS来实现。
我们先根据现有的一些认识,包括独享锁和共享锁的定义,刚刚重入锁的原理,推导出大概的思路并不困难:
-
使用三个变量分别记录 共享重入次数(readCount),独享重入次数(writeCount)已经独享锁的持有者(owner)
-
存放抢锁失败的线程的等待队列(waiters)
有了这三样东西考虑一下逻辑,为了方便理解下面将共享线程理解为获取读操作的线程,将独享线程理解为获取写操作的线程。
当有线程试图获取锁时时,根据不同的线程类型做不同的逻辑操作:如果是写线程进入,那么就去判断readCount是否为0,若不为0,说明已经有线程占有了,当前线程直接进入等待队列,若为0就判断writeCount是否为0,若为0就CAS修改值,并且把owner修改为自己(当前线程),若writeCount不为0 就去判断owner是否为当前线程,若为当前线程就去做重入操作,如果不是自己就乖乖进入等待队列;如果是读线程就去判断writeCount是否为0,如果不为0的话 这里要注意一下,这时候会去判断owner是否为当前线程,如果为当前线程就进行锁降级,如果owner不是当前线程那就进入队列等待 ,如果writeCount为0 那么就去对readCount进行CAS操作修改值,(读线程不需要记录owner)。
这里稍微解释一下为什么读操作去判断写的变量,而写操作去判断读的变量:因为读写是互斥的,拥有读锁的时候不能获取写锁,拥有写锁的时候是可以通过降级获得读锁的。
解锁过程相对来说和单纯的重入锁没有特别的地方,记住由于重入的特性,只有当count变为0的时候,我们才会去修改owner的值,这里也是要使用CAS 进行修改的。等待队列中做了判断如果首部线程为读线程会试着唤醒队列后一位的读线程,但不会唤醒写线程,这点是必须保证的。
那么这里提一个问题,对于获取独占锁的时候我们会先判断readCount是否为0 然后再去判断writeCount是否为0对吧,那么在这个过程中如果有线程获取到了共享锁的话,那么就出问题了吧,这里的操作是有可能被分割了。 那么JDK中是怎么解决这个问题的呢?
JDK中很巧妙地使用了一个int值得高低为来表示线程是读还是写,低位标记独占锁,高位标记共享锁。
截图是中ReentrantReadWriteLock的部分代码注释
好的,现在我们了解完了ReentrantReadWriteLock和ReentrantLock之后,回到AQS。
相信在翻看源码的时候,你一定会看到这两个类中有很多共同点:都有等待队列,重入计数变量,加解锁操作函数。类似这样重复东西我们自然要把代码给拿出来复用。
翻看一下不难发现在这两个类中分别都有如下的抽象内部类,没错 这就是JDK 所实现的代码复用,这么久了终于出现了我们的正主--AbstractQueueSynchronizer。
ReentrantReadWriteLock:
ReentrantLock:
让我们看看这个Sync里面都有什么,截图略... 正是一些获取锁与释放锁的方法呢,希望读到这里AQS的模样能够在你脑海浮现得更清晰一些。
让我们再来看看AQS的结构
AQS 的主要组成部分如下所示:
最后来总结一下:
AQS是JDK提供的一个抽象类,提供了对资源的占用释放,线程挂起、唤醒的基本逻辑;并且预留了前缀为try*的方法让用户自定义实现逻辑;通过继承AQS得到特定的同步器支撑起各种业务场景以实现不同(诸如ReentrantLock/CountDownLatch/ReentrantReadWriteLock)的锁实现。