手撕AQS之独占锁模式

写在前面

AQS 也是在来来回回看了源码好多遍,才有所理解。原本打算就这一篇写完,点到即止,但不知觉的又深入了,无法抗拒的代码魅力啊!所以分为两篇,一篇共享式,一篇独占式,美滋滋呢。共享式已写好,共享式中有关于我个人对 AQS 的理解,这篇独占式就直接分析源码了。


这里分享一种很有用的断点源码的方式:那就是使用 Intellij 的书签,本身在调试源码的时候,很容易就乱掉层次,通过书签,我们可以先把代码一层一层的排好序,这样就不容易迷糊了。下面是我在理解 AQS 独占锁时的书签截图:
独占式书签图

1,2,3 代表释放锁,A, B … 代表加锁的过程;

有了这个就相当于目录了,再通过下图我就可以实现在代码中穿梭了:

独占式详细代码书签图

通过图中的排序功能,整个流程就这样串起来了;

由于 AQS 本身是个抽象类,没有具体的语义,所以我结合了 ReentrantLock 去理解它。


独占式的锁获取

独占式锁的获取其实和共享锁流程上差不多,下面是它的主要流程图:

独占锁获取流程图

下面详细分析下上诉流程:

  1. 加锁:由于这里是从 ReentrantLock 入手的,所以,这步指的是 Reentrant 内部的一个继承了 AQS 的类( NofairSync )的 lock 方法;
  2. 请求锁:这步是 AQSacquire 方法;
  3. 尝试请求锁:这是在上面的请求锁中调用的,但交由子类实现;
  4. 入队列:当尝试请求锁失败,就会入队列,线程会进入 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 本质上是依赖于对 statecas 操作成功与否,来判断是否成功获取锁的,节点中的线程有机会 unpark 掉,但是不一定能够结束,获得执行机会,因为它还需要重新循环,在拿到锁以后,才能结束整个流程,去执行自己的业务代码,否则,它还是会重新 park 的。

CLH 队列呢,则维护了这些 park 线程,根据 waitStatus 来维护其状态。当队列中有节点线程被唤醒并成功获得锁以后,就会被设置为头节点,修改其属性值,这个节点的线程就可以得以继续执行了,它的加锁生涯就结束了,后续再去释放下锁就好了。释放锁,则接着执行队列中的唤醒,唤醒后,仍然请求加锁,锁获取成功则结束线程加锁生涯,失败,则又陷入 park 状态,等待被唤醒;


推荐博文


手撕AQS之共享锁模式


参考博文



我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
ding:20px’>参考博文



我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值