从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实例通过计数器的方式达到了共享一把锁的效果,只有所有的线程都执行完,才释放这把锁,这就是共享锁的概念。


浙公网安备 33010602011771号