从Lock看AQS

不扯概念,直接看代码:

以可重入锁ReentrantLock的lock()方法切入

 

 

 Sync是ReentrantLock的一个抽象的静态内部类,同时继承了AbstractQueuedSynchronizer也就是我们常说的AQS,同步队列,源码上的解释太长了,这个抽象类的作用,就是维护一个双向链表,节点为node,每个节点有可见性的状态值,还有当前线程的引用,然后根据一个状态值来判断当前线程是进行park操作,还是unpark操作

 

 

 

看下AQS的属性,Node就是双向链表的每个节点,标记的两个方法是维护双向链表的方法

 

 

 再看下Node属性,几个属性节点,以及pre,next,waitStatus属性值,后面看各自的作用

 

 

 锁有公平锁和非公平锁两种实现,看下代码就清楚什么是公平锁与非公平锁

 

 

 NonfairSync的lock方法:

 

 

 

FairSync的lock方法:

 

 

 区别就在非公平锁多了一个cas方法

CAS方法是一个安全的原子操作,通过对比目标值与期望值是否相当,如果相等就更新的一种机制,和数据库的乐观锁是一样的,是直接通过一个偏移量拿到内存中的值作对比,避免了不一致的可能性。底层是一个native方法

非公平锁的CAS方法就是将当前锁对象的state值设为1,并且将exclusiveOwnerThread属性设为当前线程,这其实就是锁资源的占有,这样第二个线程进入后,CAS就未false然后只能acquire(1)方法。不过CAS是有可能成功的,也就是说第二个线程进来,不论如何是做了一次锁抢占的,先不排队,直接抢占一次,抢到了,那就获得了锁资源,不会被阻塞了

而公平锁,就是直接执行了acquire(1)方法,没有CAS操作,这就是公平和非公平锁了。

 

 

 现在设立场景(只看非公平锁):

当前第一个线程执行了lock()方法,CAS成功,我们设第一个线程为threadA线程,此时当前锁的state值为1,exclusiveOwnerThread为threadA

在第一个线程还在执行的时候,第二个线程threadB进入,CAS不成功,进入acquire(1)方法:

 

进入方法:可以看出,先再次尝试了一次CAS锁竞争,因为此时state是1所以第一个if不成立,同时current == getExclusiveOwnerThread()肯定也不成立,所以返回了false。

这段代码同时也解释了可重入锁,因为真是场景,多线程时间的访问是抢夺cpu时间片,说直白了就说随机的,所以存在current == getExclusiveOwnerThread()为true的情况,这时候不需要在进行锁竞争,直接在state上做累加即可,这就是可重入,同一个线程,锁资源由当前线程继续持有

 

 

 如果条件都不成立,则就要进入阻塞队列,此时的执行进来的是线程threadB

 

 

enq()方法是一个自旋式的初始化队列的方法

 

 

 

简单画个图,补充一点,初始化的节点waitStatus属性为默认值0

 

 

 

一个阻塞队列已经形成了,然后可以想到应该就算把线程threadB给park起来,继续往下看 

这些方法,不需要去看注解,直接看代码完全能看的清楚:

方法的入参node,就算维护了threadB的节点node,arg为1

方法内也是自旋,通过判断当前node的前一个节点是否是头节点,并且,再执行以下nonfairTryAcquire方法,判断是否是可重入的,获取锁资源抢夺成功。因为目前锁资源threadA占有,所以维护thredB的node肯定抢夺不成功的,继续向下

注:两个标记处,一个自旋的获取,还有一个是节点的取消

当线程没有被阻塞的时候,头节点的下一个节点一直是自旋式的尝试获得锁,如果被阻塞,自旋暂停,被唤醒后再次自旋的获取锁,结果要不获取锁成功,要不节点被取消。如果获取锁成功,则当前节点设为头结点,将头结点GC,然后下一次又是头结点的第二个节点自旋获取。

 

 

 

 

 进入方法shouldParkAfterFailedAcquire,入参为head节点,和threadB所在的节点,方法返回false,并且将head节点的waitStatus赋值为-1

注:解释下其余逻辑,如果此时链表已经有一定的长度了,当w>0说明当前节点的pre节点已经被标记取消状态了,

do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);

上面代码的作用主要是将被取消的节点跳过去,直接把当前的节点已往前第一个不是取消状态的节点建立关系,可以理解清楚无用状态的节点。
当前场景,只有连个节点,w的状态又为0,所以不冲突此段逻辑

 

 

 shouldParkAfterFailedAcquire返回了false,进入自旋,在进入shouldParkAfterFailedAcquire方法时,已经满足第一个条件,返回true,

调用parkAndCheckInterrupt()方法,将线程threadB挂起,

Thread.interrupted()方法,是线程的一种响应式的中断方式,此处检查线程是否中断成功

此此threadB被阻塞了。

 

 

 此时如果有线程ThreadC进来,如果没有直接CAS成功的话,就要进入到阻塞队列,成为tail节点,也被阻塞。

 

下面分析下阻塞线程的唤醒

线程的唤醒,通过锁的unlock方法:

 

 

 

 锁的释放没有公平锁和非公平锁的区分了,下面方法tryRelease(),threadA线程进入此方法,说明已经执行完毕,已经不需要锁资源。

标记处处理了可重入锁的情况,释放锁资源,就是做了两件事情,当前线程设为null,state值设为0

 

 

 释放成功后,拿到head节点,此刻的链表就是上面产生的链表,head节点存在且waitStatus为-1

进入唤醒操作unparkSuccessor()

先将head节点的状态值设为0

然后拿到节点s,即为维护threadB的节点

正常的时候,threadB直接unpark被成功唤醒

如果不存在ThreadB节点,或者ThreadB节点的状态为取消,则从tail节点开始向前遍历,找到最近的一个非取消节点,进行唤醒

这里从尾结点遍历时为了防止出现节点为null的情况直接跳出循环,线程节点为null这种情况,有必须要考虑的,需要被唤醒的线程没有被成功唤醒,从尾部开始,只要存在需要被唤醒的线程节点,就一定能够遍历的到。

至此,线程threadB被成功唤醒

 

可以看出,整个过程,AQS作为一个参与者,本身并不操作线程,只是通过一个链表维护线程一个外部状态,然后通过状态值的改变,决定线程是唤醒还是阻塞状态。

顺带提一下共享锁的实现

CountDownLatch countDownLatch = new CountDownLatch(10);
countDownLatch.countDown();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
new CountDownLatch(10)构造入参会改变锁对象的state值,就是作为一个计数器
countDownLatch.await()构建一个链表,并在头部加入一个共享标记的节点node,并将当前线程放入head.next节点,和上面lock的实现一样,在没被阻塞前一直自旋,直到被阻塞。

 

当countDown()将state值减为0后,则释放锁

unparkSuccessor()方法通用,上面的自旋过程中释放锁用的也是这个方法

这样,多个线程持有一个CountDownLatch实例通过计数器的方式达到了共享一把锁的效果,只有所有的线程都执行完,才释放这把锁,这就是共享锁的概念。

 

 

posted @ 2021-05-08 21:16  好好的一个居士  阅读(92)  评论(0)    收藏  举报