ReentrantLock和AQS源码解读系列一
ReentrantLock
ReentrantLock一直是AQS使用的代表,很多人应该都用的比较熟了,但是内部的一些细节可能还不是很清楚,而且随着JDK版本的一直升级,AQS内部的一些源码也有了一些改变,以前很多操作是unsafe的,现在变成了使用VarHandle,我用的是JDK11,里面已经是改变了,据说VarHandle是以前unsafe的替代版,很厉害的样子,其实功能跟unsafe操作差不多,算是加强版吧,具体的可以百度学习下,既然换了新的,肯定有好处,有道理。当然这个不是重点,重点还是要理解AQS的原理,怎么个自旋获得所,怎么个排队等等。
ReentrantLock的独占,非公平锁,无条件的简单例子
我们用个最简单的例子来看看具体怎么执行的,我们用独占,非公平锁,无条件,举个简单的例子:
public class LockTest {
private static ReentrantLock doctorLock = new ReentrantLock();
private static class PatientThread extends Thread {
@Override
public void run() {
doctorLock.lock()
try {
System.out.println(Thread.currentThread().getName()+"尝试获得锁");
System.out.println(Thread.currentThread().getName()+"获取成功做事");
} finally {
System.out.println(Thread.currentThread().getName()+"释放锁");
doctorLock.unlock();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new PatientThread().start();
}
}
}
获取锁细节
首先我们会看到ReentrantLock 初始化会判断是否要用公平同步器,公平即新来的需要排队,不公平即新来的可能会插队,插队失败还是得排队:

默认不传是非公平的:

调用lock,就是调用了刚才的非公平同步器的acquire,传入参数是1,表示想给锁状态加1,默认是0表示锁空着:

再进去,可以看到,会先尝试获取锁,这里就有讲究了,我们一步步分析:

然后如果刚好是调用公平同步器的方法,这里我们想获取锁:

然后我们看非公平的:

其实就是多了个是否要先看看有没人在排队hasQueuedPredecessors,再去获得资源。
我们以非公平来说下,因为比较相对还比较容易理解。
锁可获取
如果锁空着,就尝试获取锁compareAndSetState(0, acquires),如果成功了,那么直接就设置当前线程独占,然后返回,就可以做自己的事啦:

锁不可获取但是可重入
如果锁不可获取,但是当前线程就是独占锁的线程,那就把锁状态加1,然后返回做自己的事啦,这里就是可重入的表现:

同一个线程可以多次获得锁,当然释放锁的时候也是多次,否则是不会唤醒后继结点的。而且这里设置状态不需要原子操作,因为就一个线程能进来。
排队获取锁
添加结点addWaiter

可以看到,这里其实是延迟队列初始化的,如果队尾还是空的话才会初始化队列initializeSyncQueue,然后自旋去排队。我们先看看初始化队列做了什么:

其实就是创建一个空节点,作为头结点,也作为尾节点,但是这里是原子操作,因为可能有多线程同时想初始化队列,只后一个会成功,其他的失败也没关系:

然后就是要把结点添加到队列里,也是原子操作compareAndSetTail(oldTail, node),因为会有多线程想同时添加到队尾,但是同一时刻只能一个,其他的失败了自旋继续添加,成功为止:

很多情况可能这样,CAS还没开始或者失败的时候:

不过最后都会这样:

自旋获取锁acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;//被中断过的标志
try {
for (;;) {
final Node p = node.predecessor();//获取前驱结点
if (p == head && tryAcquire(arg)) {//如果前驱结点是头结点且尝试获取锁成功,说明前驱结点已经释放锁了,那就可以删除了
setHead(node);//重新设置node为头结点
p.next = null; // help GC 把以前的头结点删除,设置null帮助GC回收
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))//判断是否要阻塞,把前驱状态设置后下一步自旋就要阻塞了
interrupted |= parkAndCheckInterrupt();//阻塞并且等待唤醒或者中断,返回时查看是否是被中断的
}
} catch (Throwable t) {
cancelAcquire(node);//出了异常会被取消
if (interrupted)
selfInterrupt();//处理中断
throw t;
}
}
已经在队伍里的只能自旋尝试获取锁,或者阻塞:

首先尝试获取锁,但是这个是有前提的,那就是你的前驱是头结点,你才有这个资格去尝试获取锁,这个也是队列的意义,先进先出,如果成功了,那就把头结点删除,自己成为头结点:

然后返回继续做事。如果没成功,那就要准备阻塞了shouldParkAfterFailedAcquire:

先看前驱的状态:
如果是SIGNAL说明,前驱结点已经知道释放锁的时候会唤醒后继,所以当前结点就可以准备阻塞了,返回。
如果是CANCELLED,那说明处于取消状态,就跳过他,去前面找非取消的作为前驱。这里也就是在阻塞前会去把前面取消状态的清除出去。
如果是其他的,那就给前驱打标签pred.compareAndSetWaitStatus(ws, Node.SIGNAL);,告诉前驱释放锁的时候唤醒我。
然后如果要阻塞,就调用parkAndCheckInterrupt,进行阻塞:

用的是LockSupport.park,这个是可以被unpark唤醒或者中断的,如果有中断的话可以返回出去,如果没有那就只是唤醒,之后再进行自旋循环,直到获取锁或者被取消了才会跳出自旋。
这里就是前面预备知识里讲过的,获取锁的过程时不会处理中断的,就算被打断了,也是继续自旋去获取锁。只有获取锁或者取消之后,才会进行中断的处理,也就是对应:


里面也就是Thread.currentThread().interrupt();设置中断标志位。这样后面的应用程序就可以根据这个标志位做处理了。
非公平锁插队
前面自旋的过程也是获取锁,但是如果是非公平锁可能会被插队,为什么呢,我们看到进来的时候就可以尝试获取锁:

自旋的时候也可以尝试获取,这两个人操作不是同步的:

因为非阻塞尝试获取里面没有判断前面排队情况:

也就是会出现抢CAS原子操作,就可能会出现插队的情况,不过没关系,完成后还是会唤醒头结点的后继结点,继续抢锁。
释放锁
前面讲了下获得锁的一些流程,然后之后就会继续执行业务代码,直到释放锁,传递的参数也是1,也就是释放一次,因为可能有重入,计数器会累加,当然也可能要多次释放啦:


首先是尝试释放锁,这段应该还是比较好懂的,这里没有什么多线程,因为只有你拿到锁,单线程操作,计数器尝试减1,然后会有个判断,如果不是独占线程就报错,避免其他线程无脑调用这个方法,然后就是判断计数器是否是0,如果是就要释放锁了,当然独占线程也变空了:

随后就是这段:

表示如果头结点不为空,状态不为0,就需要去释放头结点的后继结点:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)//把头结点状态设置回无状态,当然失败也没关系,否则就该自旋修改了
node.compareAndSetWaitStatus(ws, 0);
/* 如果下个结点不存在或者下个结点是取消状态 就遍历,唤醒下一个非取消状态的
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;//获取下一个结点
if (s == null || s.waitStatus > 0) {//如果是空就说明不用做什么,如果是取消的,尝试找到非取消结点进行唤醒
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;//寻找后继中最近的且非取消的结点
}
if (s != null)//找到就唤醒结点中的线程
LockSupport.unpark(s.thread);
}
主要是想办法唤醒后继线程,如果后继线程取消了,就找后继的后继,找到就唤醒。
这里其实也会有一些细节的地方,因为多线程LockSupport.unpark其实不一定是唤醒阻塞的线程,也就可能,此时那个还在自旋,不过没关系,自旋要么获取锁成功,要么阻塞。等到阻塞的时候发现前面已经有unpark过,因此调用park其实是没作用,也就不阻塞了,然后去尝试获取锁,此时锁已经被释放了,如果没人插队的,很可能就获得到所了。
总结
本篇介绍了AQS的一些基本流程,其实往简单的说就是利用原子操作来抢资源,抢不到的要排队,如果是公平锁就一定要排队,如果是非公平的,可以尝试不排队,但是失败了还是要排队。排队的过程中会进行自旋,会去设置前驱的状态,然后阻塞,前驱释放锁的时候就会换新后继。还有整个获取锁的过程是不响应中断的,只有最后获取之后才会告诉你是否有过中断,然后去进行中断。
推荐
好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。
本文深入解析ReentrantLock及AQS的工作原理,包括独占、非公平锁的使用,自旋与排队获取锁机制,以及锁的重入与释放过程。通过实例展示线程竞争、阻塞与唤醒的细节。
170万+





