写在开始
ReentrantLock 是 java 1.5 以后新加入的除 synchronized 关键字以外的新的加锁方式,从其命名上就可以知道它也是用来加可重入锁。在使用层面上它具备了synchronized所具有的所有功能,并且由于它支持响应中断、超时等操作,所以在某些场景下它会比synchronized更加灵活。
ReentrantLock和synchronized在使用性能上是否有太大的差距呢?关于这个问题,由于我本人没有亲自实验过,所以不得而知。
但是,我估计二者应该没太大差距,随着jdk版本的不断迭代,synchronized只要用的好,基本没有性能上的瓶颈问题。而且最主要的是,jdk推出ReentrantLock并不是为了取代synchronized,而是为了弥补synchronized在某些使用场景上的不足。
在实现原理上,ReentrantLock基于AQS,而synchronized是监视器。关于这两个之间的区别,大家可以在网上搜一些相关文章看一下。如果不清楚它们也别慌,只要知道AQS是用java写出来的,监视器是JVM实现的就行。
所以呢,既然AQS是java代码写出来的(也就是人眼能看懂喽),那么,这篇文章就通过研究ReentrantLock加锁、解锁的源码,顺带来看一下AQS底层是怎么来实现支持的。
简述AQS
AQS是AbstractQueuedSynchronizer这个java类的首字母简称。这个类几乎是juc下所有并发工具的基础。
它的核心思想是,内部维护一个共享变量(state)和双端队列(GLH队列)。各个工作线程在加锁时会去争夺获取该变量,当该变量被某个线程占用时该变量的值就会被修改(初始时state=0,修改后state>0),则表示该线程现在可以执行,未获取到该变量的线程将会进入到双向链表中排队等待(同时线程本身被阻塞),而后待共享变量被释放后(state=0),队列中被阻塞的线程会依次被唤醒执行。
ReentrantLock 使用
我们先来看下ReentrantLock在日常中的使用方式。
public class ReentrantLockTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
try{
lock.lock(); //加锁
//搞点事情。。。
}finally {
lock.unlock();//解锁
}
}
}
上述示例,简单展示了ReentrantLock加锁、解锁的过程(记住一定用完要unlock,这一点确实不如synchronized)。当然它还支持更加多样的锁使用方式,如公平锁、非公平锁、超时加锁等等。
好了,简单看过后,接下来就点进去看下实现原理吧。
看源码
加锁过程
我们顺着上述示例中的lock方法进去,进入到ReentrantLock.java文件中
// ReentrantLock.java
//ReentrantLock#lock
public void lock() {
sync.lock();
}
其内部调用了ReentrantLock类成员变量sync的lock方法。sync何许人也?下面是它在ReentrantLock类中第一次出现的地方。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
OK,现在明确了,这玩意原来是个抽象类,然后那个lock方法就是它的抽象方法,于是我们继续找到实现这个抽象类的具体类。
//ReentrantLock.java
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
//......省略很多行
//ReentrantLock.java
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//......省略很多行
看到这些内部类的名字,此时或许你已经想到了,这应该就是实现公平锁和非公平锁的基础,我们接着在找下这两个类被实例化的地方。
// ReentrantLock.java
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
好了,确认无疑了。原来new ReentrantLock创建公平锁和公平锁时传递的那个boolean变量的背后就是在实例化不同的Sync实例。
现在在公平锁和非公平锁之间,我们选择公平锁来进行接下来的“细细品味”。因为理解了公平锁的实现原理,再回过来看非公平锁的实现过程就一目了然了(这个世界本身对于公平规则的制定就是一件非常难的事情)。
下面我们看下FairSync#lock的实现方式。
// ReentrantLock.java
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
可以看到,它实质调用了一个acquire方法,继续跟进这个方法。
//AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们发现,acquire来自AbstractQueuedSynchronizer类(之后简称AQS类),而这个类就是AQS的由来和核心,也就是现在我们现在即将要探究AQS的实现原理了。
从表面看acquire方法貌似没有太多复杂的实现逻辑,下面我们分别看下其内部调用的这些方法的实现细节。
tryAcquire()
// AbstractQueuedSynchronizer.java
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
进去发现,AQS类并没有对其进行具体实现,于是开始寻找它的子类。我们发现FairSync和NonfairSync是AQS的子类,并对tryAcquire方法进行了具体的实现。
// ReentrantLock.java
// FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前state状态
int c = getState();
if (c == 0) {
//是否队列中有前序节点节点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//设置当前线程独占
setExclusiveOwnerThread(current);
return true;
}
}
//当前线程和队列中独占线程是否相等
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
上述源码展示了FairSync内部类tryAcquire的具体实现。这里首先说明几个关键方法:
- getState():在AQS类中有一个成员变量state(如下所示),用于标识共享资源(见简述AQS)。该方法用于获取当前共享资源的值。
// AbstractQueuedSynchronizer.java
/**
* The synchronization state.
*/
private volatile int state;
- setState():同1,该方法用于设置共享资源state的值
- setExclusiveOwnerThread():在AQS父类AbstractOwnableSynchronizer中有一个成员变量(如下所示)用于标识当前独占线程,独占线程即表示当前获得state资源且正在运行的线程。该方法用于将参数中传入的线程标识为独占线程。
// AbstractOwnableSynchronizer.java
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
getExclusiveOwnerThread():同2,该方法用于获取当前的独占线程。
hasQueuedPredecessors():判断当前CLH队列中是否还有等待中的线程(关于CLH队列,见简述AQS)。
compareAndSetState():通过CAS方式设置共享资源state的值。
下面我们先看下hasQueuedPredecessors()方法的内部实现:
// AbstractQueuedSynchronizer.java
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
该方法由AQS类提供具体实现,内部的执行逻辑主要用于对CLH队列中的节点进行判断。其具体的判断逻辑是:
- 若头结点等于尾节点(h==t)返回false。此时表示队列为空,即没有正在执行中的线程且前序没有等待中的线程。
- 当头结点不等于尾节点(h!=t)时,若如下两个条件任何一个满足,则返回true,表示当前队列中存在排队的线程节点。
a. 若头节点的next节点为空((s = h.next) == null)。
b. 头节点的next所对应的线程不是当前线程(thread != Thread.currentThread())。
注意: 此时你可能会有疑问,为什么“头节点的next节点为空”也标识队列中存在排队的节点呢?
之所以有这样的考虑的原因主要有两点:
- CLH队列的头结点是一个虚拟节点,它的作用只是用来占位,并不代表正真意义上的线程节点,因此队列中的第二的节点才是真正的线程节点。
-
当前线程(A线程)执行到上述方法逻辑时,正好有另一个线程(B线程)比当前线程执行要快,且执行“入队”的操作,但是由于是双向队列,因此入队需要两步骤的操作,即第一步,B线程节点的pre指向head节点;第二步,head节点的next指向B线程节点。但是此时B线程只执行到第一步,第二步还没有执行,那么这个时候就出现了h.next==null的现象了。
下面梳理一下tryAcquire方法的执行流程:
从以上流程图上看,同一个线程在已经是独占线程的情况下,可以重复加锁,这也就是所谓重入锁的来源。
addWaiter()
下面我们接着看下AbstractQueuedSynchronizer#acquire方法逻辑中的addWaiter方法内部。
// AbstractQueuedSynchronizer.java
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
这个方法也是AQS类提供的方法,从方法逻辑上大体可以看出,它是在进行一些队列操作。关于Node对象在此需要简单说明一下:
- 我们之前一直在提的CLH队列中的每个节点实质是一个Node对象,该类中封装了前、后指针,当前线程、节点状态等信息。
我们先看一下上述方法中除enq()方法之外的逻辑:
- 首先new了一个关于当前线程的Node对象
- 然后将CLH队列的尾节点赋值给pre,并判断pre是否为空
- 如果pre!=null则表示当前的CLH是一个非空队列,此时对第1步创建的node对象进行入队操作。如果入队成功则返回当前node,如果失败则调用enq()方法
- 如果pre==null,则表示当前CLH队列为空,此时调用enq()方法
下面看一下enq()方法的内部逻辑:
// AbstractQueuedSynchronizer.java
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
同样enq()执行逻辑也由AQS类自己提供。方法开头便是一个由for死循环构成的一个自旋操作,而后的执行逻辑如下:
- 判断当前队列的tail节点是否为null,如果为空的话在队列中设置一个新new的Node节点作为队列的head节点,同时tail也指向新创建的head节点(注意:之前提到的,CLH的头结点是一个虚节点,它不包含任何线程,只用于占位)。
- 如果tail节点不为空,则将参数传入的node节点(即addWaiter方法中创建的封装了当前线程的Node对象)入队到CLH队列中。
- 由于enq方法内部是一个自旋操作,所以最终都会走到第2步逻辑中。
通过上述的分析,我们不难发现enq方法本质是通过自旋转的方式将封装了当前线程的node加入的CLH队列中。
现在我们来梳理一下整个addWaiter方法的执行逻辑:
acquireQueued()
接下来我们接着看下AbstractQueuedSynchronizer#acquire方法所调用的acquireQueued()方法的执行逻辑。
// AbstractQueuedSynchronizer.java
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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循环吧。没错,又是一个自旋操作。我们先描述一下方法里面的逻辑:
- 方法中有两个局部变量failed用于表示线程加锁失败,interrupted用于表示线程是否中断
- 方法的第一个参数Node是刚才addWaiter()方法中封装好,且加入到等待队列中的包含当前线程的Node
- 方法进入自旋逻辑后:
- 获取当前线程node节点的前驱节点(node.predecessor()),判断它 是否为head节点,如果是的话尝试去获取锁(tryAcquire()),获取成功后,将当前node设置为head节点(setHead()还记得之前说CLH队列中的头结点其实是一个占位节点吗,因为头节点中不包含任何线程,此处也一样,看下setHead方法中的具体逻辑就一目了然了),然后返回当前线程是否中断(return interrupted)
- 如果当前线程的前驱节点不是head节点,则调用shouldParkAfterFailedAcquire()、parkAndCheckInterrupt()方法。
下面我们分别进入shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()方法内部看下里面的逻辑。
-
shouldParkAfterFailedAcquire()
从该方法名字面上理解,这个方法的作用似乎是返回一个boolean值,该值表示获取锁(acquire)失败(failed)后(after),是否应该中断(park)当前线程。下面看下代码:
// AbstractQueuedSynchronizer.java private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
纵观方法逻辑,主要是对Node中waitStatus字段值进行判断,然后对队列中节点进行相应操作。关于waitStatus值的的具体含义,大家可以查看下Node类中具体的定义的静态常量,结合上述方法逻辑这里简单描述下:当waitStatus>0时,表示对应的Node时cancelled状态;当waitStatus=-1时,表示需要将该节点(waitStatus=-1)的后继节点中的线程中断(park),即也侧面反应了该节点线程处于待唤醒执行。
那么我们来描述下shouldParkAfterFailedAcquire()方法中的具体逻辑:
1. 获取pre节点(当前线程节点的前序节点)waitStatus值
2. 若该值等于-1,表示需要将当前线程中断(park),因此返回true
3. 若该值大于0,表示pre节点处于cancelled状态,从当前线程节点开始,依次向前将所有的waitStatus>0的节点从CLH中移除。
4. 如果不符合2、3步骤的条件,则将pre节点的waitStatus通过CAS的方式设值为-1因此通过上述的逻辑描述,也确实印证了该方法的目的确实返回当前线程是否需要中断,此外其中还加了一些逻辑用来剔除掉CLH队列中处于取消状态的节点
-
parkAndCheckInterrupt()
同上,我们从字面意思上理解,该方法用于中断(park)当前线程,并且检查(check)线程的中断状态(interrupt)。
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
从方法逻辑上看很简单,调用了LockSupport.park方法,而后返回当前线程的中断状态。
注意,当方法执行到LockSupport.park(this)这一步时,整个当前线程就阻塞了,也就是需要等待前序节点来唤醒它,然后才能继续执行Thread.interrupted()的逻辑,以及acquireQueued()和acquire()剩余的逻辑。
也就是当该线程被唤醒以后,会继续将parkAndCheckInterrupt()方法执行完,在acquireQueued()方法中根据parkAndCheckInterrupt()返回结果对interrupted进行设值,而后继续进入到自旋逻辑中去获取锁资源。
下面我们再回到acquireQueued()方法中(再贴一次代码)。
// AbstractQueuedSynchronizer.java
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);
}
}
这个方法的最后有一个finally逻辑,也就是acquireQueued()方法返回前一定会执行的一个逻辑。当failed=true的时候,会调用cancelAcquire()方法。
从方法上看failed在声明时的默认值是true,当前线程节点获取锁成功后,该值就变为false,那么也就是只有在当前线程获取锁失败的情况该值才会true,即当前线程节点获取锁失败的情况下会触发cancelAcquire()方法逻辑。
那么,我们再探究下这个方法里的执行细节:
// AbstractQueuedSynchronizer.java
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
这里我们文字描述下,这个方法的执行逻辑:
若当前节点为空,则直接return
将当前节点的Thead属性设置为null
声明一个pre变量,值为当前节点的前序节点
-
从当前节点开始,一直向前寻找到一个waitStatus>0(非取消)的前序节点(和shouldParkAfterFailedAcquire中的逻辑非常相似),当然寻找的过程中也操作了队列,使得当前节点的前序节点(node.prev)和pre变量最终指向了它前面的非取消节点
将当前节点的waiStatus设值为取消状态(node.waitStatus = Node.CANCELLED)
-
如果当前节点是CLH队列的尾节点,那么就将它的pre节点(3步骤寻找到的)设置为CLH的尾节点,并将队列尾节点的next指向null,如下图所示:
-
如果当前节点的前序节点(之后简称pre)不是头结点(即当前节点处于CLH中间),且pre的waitStatus=-1(Node.SIGNAL),或pre.waitStatus<0则将其设置为-1(Node.SIGNAL),并且pre节点中的线程不为空,那么将pre的next指向当前节点的next
-
如果当前节点是头节点,那么唤醒它的后继节点线程(因为本应该是唤醒当前节点线程,但是失败了,所以让后继节点继续执行)。
综上cancelAcquire方法的执行逻辑,我们也梳理结束了,整体看着较为复杂,其实无非就是一些常规的队列操作,看到了无需紧张。
那么,上述我们分别讲述了AbstractQueuedSynchronizer#acquireQueued()方法内部的执行细节,由于走的很深,下面我们来梳理一下整个acquireQueued()方法的执行流程:
我们再来回头看AbstractQueuedSynchronizer#acquire,先回顾一下整个加锁的执行链路
以公平锁为例:
ReentrantLock#lock --> FairSync#lock --> AbstractQueuedSynchronizer#acquire
再贴一下acquire()方法的代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到,整个acquire加锁的逻辑就是,先通过tryAcquire尝试去获取锁,若失败,则进入CLH队列并阻塞当前线程,等待被唤醒,此时acquire方法的执行也就被hang住了。当线程被前序节点唤醒后acquire方法的逻辑继续执行(并根据acquireQueued方法返回结果来确定是否调用selfInterrupt()从而设置当前线程的中断状态),而后继续执行线程的处理逻辑。
上述就是整个公平锁的加锁过程,关于非公平锁的加锁过程和公平锁基本相似,只是在加锁时没有判断当前队列是否有前序节点,感兴趣的可以自行查看一下。
解锁过程
下面我们再来看下ReentrantLock的解锁过程,非公平锁和公平锁的公用了一套解锁逻辑。我们先简单梳理一下解锁过程中的方法调用链路:
解锁过程
ReentrantLock#unlock ---> AbstractQueuedSynchronizer#release ---> Sync#tryRelease
整个解锁过程没有加锁的过程那么复杂,我们直接先看下AbstractQueuedSynchronizer#release方法
// AbstractQueuedSynchronizer.java
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
方法逻辑很清晰:
- 首先调用Sync#tryRelease释放锁资源
- 释放成功后,将CLH队列的头结点取出,并判断头结点不为空,且非取消状态(h.waitStatus != 0),而后唤醒头结点的后继节点(unparkSuccessor)。
先来看下Sync#tryRelease方法源码:
protected final boolean tryRelease(int releases) {
//获取当前共享资源的值,并与releases相减
int c = getState() - releases;
//判断当前线程是否是当前的独占线程,若不是则直接抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果当前共享资源值和release相减后等于0,
//则表示资源可被释放,并将当前的独占线程设置为空,
//并将方法返回值设为true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//设置state的值
setState(c);
return free;
}
整个的执行逻辑也不复杂,具体的逻辑我在上方中加了注释。总之,当该方法返回true则表示当前共享资源无任何线程占有,即,此时可以唤醒下一个队列中线程。
我们再来看下unparkSuccessor方法源码
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)
compareAndSetWaitStatus(node, 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 t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
该方法由于加了一些队列的操作,所以可能看上去有些复杂。下面我们来梳理一下:
- 此时方法中传入的node节点是CLH的头结点(之后都称node为头结点)。
- 获取头结点的waitStatus,若小于0,则将其设为0(0可以理解为是一个无用节点)
- 将头结点的next节点赋值给s
- 若s等于空,或者s.waitStatus>0(表示s节点处于cancelled状态),则从队列尾部开始向前一直寻找到头节点之后第一个待唤醒节点(即waitStatus <= 0的节点),并将该节点赋值给s。
- s不为空,则将s对应的节点的线程唤醒
综上就是整个ReentrantLock的解锁过程,以及其中的执行细节。整个解锁过程还是比较直观易懂的。
写在最后
至此,我们已经将整个ReentrantLock的加锁解锁过程,以及背后的AQS是如何对其提供支持的,讲述完毕了。
整个过程如果要总结几个关键点的话,我觉得是“队列”、“CAS”、“自旋”、“共享资源state”。纵观其源码,由于涉及到一些队列操作,可能相对来说略为难读,但是经过梳理以后,其实现方式还是比较清晰的。希望通过理解源码,能够帮助我们在今后开发过程中更好的使用它。