写在前面
AQS
也是在来来回回看了源码好多遍,才有所理解。原本打算就这一篇写完,点到即止,但不知觉的又深入了,无法抗拒的代码魅力啊!所以分为两篇,一篇共享式,一篇独占式,美滋滋呢。共享式已写好,共享式中有关于我个人对 AQS
的理解,这篇独占式就直接分析源码了。
这里分享一种很有用的断点源码的方式:那就是使用 Intellij
的书签,本身在调试源码的时候,很容易就乱掉层次,通过书签,我们可以先把代码一层一层的排好序,这样就不容易迷糊了。下面是我在理解 AQS
独占锁时的书签截图:
1,2,3 代表释放锁,A, B … 代表加锁的过程;
有了这个就相当于目录了,再通过下图我就可以实现在代码中穿梭了:
通过图中的排序功能,整个流程就这样串起来了;
由于 AQS
本身是个抽象类,没有具体的语义,所以我结合了 ReentrantLock
去理解它。
独占式的锁获取
独占式锁的获取其实和共享锁流程上差不多,下面是它的主要流程图:
下面详细分析下上诉流程:
- 加锁:由于这里是从
ReentrantLock
入手的,所以,这步指的是Reentrant
内部的一个继承了AQS
的类(NofairSync
)的lock
方法; - 请求锁:这步是
AQS
的acquire
方法; - 尝试请求锁:这是在上面的请求锁中调用的,但交由子类实现;
- 入队列:当尝试请求锁失败,就会入队列,线程会进入
park
状态;
上面的方法,我们仅仅关心 AQS
实现的:
acquire 方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当子类的实现尝试加锁失败后,便会入队列;addWaiter
方法添加一个独占模式的节点到链尾,该节点持有了当前线程,acquireQueued
则需要操作该节点,更新 waitStatus
,这和共享锁并无区别:
acquireQueued 方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
for
循环中自旋,shouldParkAfterFailedAcquire
方法在共享锁中有讲过,它是尝试将上一节点的 waitStatus
更新为 SIGNAL
代表的值;
这个地方会做一个检测,如果当前节点的前继节点为头结点,则会尝试再去获取锁,成功的话,就将当前节点设置为头节点,每一个线程再入队列时,都会将上个节点的 waitStatus
修改为 SIGNAL
,如果线程被唤醒,在第二次进入循环时,会重新判断检测,如果是作为头节点的后继节点,那么就会再次尝试获取锁。
至于唤醒,则是在于锁的释放,如果锁释放成功,则会去 unpark
掉后继节点的线程,然后它就可以重新执行上述的循环了。
finally
中的方法,执行条件比较苛刻,分析上诉代码,只有在异常结束的情况下才会去执行到 cancelAcquire
方法,该方法负责取消当前节点,里面的逻辑也是比较多的,情况也分了很多种;
独占式的锁释放
锁释放比较简单,先尝试释放锁,成功的话,则直接释放就好了:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease
是交由子类实现的,子类初步判断,可以的话,父类再去从队列中 unpark
,但这不一定会成功,unparkSuccessor
虽然能保证将后继节点的线程 unpark
掉,但线程 unpark
后,会接着执行,也就是重新循环(上面 acquireQueued
方法),这时候如果再次请求锁失败后,那么又会重新 park
。
总结
这篇文章相对第一篇简单了很多,因为它们其实是差不多的,但是呢,独占式不用唤醒队列中的所有,它仅仅唤醒第一个就行了。
AQS
本质上是依赖于对 state
的 cas
操作成功与否,来判断是否成功获取锁的,节点中的线程有机会 unpark
掉,但是不一定能够结束,获得执行机会,因为它还需要重新循环,在拿到锁以后,才能结束整个流程,去执行自己的业务代码,否则,它还是会重新 park
的。
CLH
队列呢,则维护了这些 park
线程,根据 waitStatus
来维护其状态。当队列中有节点线程被唤醒并成功获得锁以后,就会被设置为头节点,修改其属性值,这个节点的线程就可以得以继续执行了,它的加锁生涯就结束了,后续再去释放下锁就好了。释放锁,则接着执行队列中的唤醒,唤醒后,仍然请求加锁,锁获取成功则结束线程加锁生涯,失败,则又陷入 park
状态,等待被唤醒;
推荐博文
参考博文
我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工
ding:20px’>参考博文
我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出