JUC锁原理分析之AQS核心类
Table of Contents
一、前言
队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,被认为是 J.U.C 的核心。使用AQS可以简单并高效地构造应用广泛的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS构造出符合我们自己需求的自定义同步器。
二、AQS 原理分析
1. AQS 数据结构
下图是 AQS 底层的数据结构:
从图中可以看到存在两个数据结构 Sync queue、Condition queue。
- 同步队列(Sync queue)是双向链表,是队列的一种实现,因此也可以将它当成一种队列,它内部的节点主要包含了5个属性,后续会进行描述,还包含 head、tail 节点。
- 条件队列(Condition queue)是单向链表,不是必须的,只有当程序中需要 Condition 变量时,才会存在这个单向链表,同时如果包含多个 Condition 变量时还可以存在多个条件队列。
2. AQS 的继承关系
|
从类继承关系可知,AbstractQueuedSynchronizer继承自AbstractOwnableSynchronizer抽象类,并且实现了Serializable接口,可以进行序列化。
3. AQS 类属性与内部类
-
属性
AQS 的属性很简单,其实有一个父类中的变量也很重要。
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
表示当前持有锁的线程,因为锁要进行重入。
reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
-
内部类
Node 类封装了每个线程,每个Node 实例就是同步队列的一个节点,将线程进行封装,用来等待锁资源。
注意:在阻塞队列中不包含 head 节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
Condition 的实现类 ConditionObject。(代码较长)
1 2 3 4 5 6 7 8
此类实现 Condition 接口,Condition 接口定义了条件操作规范,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Condition接口中定义了await、signal函数,用来等待条件、释放条件。
4. 类的构造函数
|
此类构造函数为从抽象构造函数,供子类调用。
5. 类的核心函数
主要以独占模式的函数进行分析。这部分主要参考,跟着这个分析来跟踪源码会很清晰。
-
acquire 方法
为了分析该函数,我们以 ReentrantLock 的 lock 函数的具体实现来进行。ReentrantLock 在内部用了内部类 Sync 来管理锁,所以真正的获取锁和释放锁是由 Sync 的实现类来控制的。
1 2
Sync 有两个实现,分别为 NonfairSync(非公平锁)和 FairSync(公平锁),我们看 FairSync 部分。
1 2 3
现在开始跟踪流程:
-
首先跟踪到 acquire 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
由源码可以知道,当一个线程调用 acquare 时,调用方法流程如下:
-
tryAcquire 方法
再来看看 tryAcquire 流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
-
addWaiter 方法
假设 tryAcquire(arg) 返回 false,那么代码将执行
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
接下来看:addWaiter(Node.EXCLUSIVE)
addWaiter方法使用快速添加的方式往sync queue尾部添加结点,如果sync queue队列还没有初始化,则会使用enq插入队列中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
-
acquireQueued 方法
继续回到这里
1 2 3
如果 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 返回true的话,意味着上面这段代码将进入selfInterrupt(), 所以正常情况下,下面应该返回 false。这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
首先获取当前节点的前驱节点,如果前驱节点是头结点并且能够获取(资源),代表该当前节点能够占有锁,设置头结点为当前节点,返回。否则,调用shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法,首先,我们看shouldParkAfterFailedAcquire方法,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
现在来看acquireQueued方法的整个的逻辑。逻辑如下:
- 判断结点的前驱是否为head并且是否成功获取(资源)
- 若步骤1均满足,则设置结点为head,之后会判断是否finally模块,然后返回。
- 若步骤2不满足,则判断是否需要park当前线程,是否需要park当前线程的逻辑是判断结点的前驱结点的状态是否为SIGNAL,若是,则park当前结点,否则,不进行park操作。
- 若park了当前线程,之后某个线程对本线程unpark后,并且本线程也获得机会运行。那么,将会继续进行步骤①的判断。
-
-
release 方法
以独占模式释放对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
三、总结
借用几个点的总结