「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。
ReentrantLock 源码解析
ReentrantLock 的核心是通过 AQS 实现的,具备了 AQS 的特征。
ReentrantLock 加锁过程
我们先说一下这例子:
以银行办理业务的案例来模拟我们的 AQS 是如何进行线程的管理和通知唤醒机制:
环境:有一个业务窗口 3 个排队的顾客;
类比(对号入座):3 个线程来模拟 3个排队的顾客,然后锁表示银行的窗口。
再来一个直观一点的示意图:
通过这个例子大家可以进行锁的调试和辅助理解,接下来我就以 ReentrantLock 中具体的方法来分析加锁、解锁过程。
代码例子:
// 以银行办理业务的案例来模拟我们的 AQS 是如何进行线程的管理和通知唤醒机制
// 3 个线程来模拟银行网点,受理窗口办理业务的顾客
// A 顾客就是第一个顾客,此时受理窗口没有任何人, A可以直接去办理
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
System.out.println(" -------> A Thread come in");
// 暂停几秒钟线程
try { TimeUnit.MINUTES.sleep(20); } catch (InterruptedException e) {e.printStackTrace();}
lock.unlock();
}, "A").start();
// 第2个顾客,由于业务窗口只有一个(只能有一个线程持有锁),此时 B进行等待
// 进入候客区
new Thread(() -> {
lock.lock();
System.out.println(" -------> B Thread come in");
try { TimeUnit.MINUTES.sleep(20); } catch (InterruptedException e) {e.printStackTrace();}
lock.unlock();
}, "B").start();
// 第3个顾客,由于业务窗口只有一个(只能有一个线程持有锁),此时 C进行等待
// 进入候客区
new Thread(() -> {
lock.lock();
System.out.println(" -------> C Thread come in");
try { TimeUnit.MINUTES.sleep(20); } catch (InterruptedException e) {e.printStackTrace();}
lock.unlock();
}, "C").start();
ReentrantLock 核心方法
ReentrantLock 的方法和结构如下图所示,不得不说 Doug Lea 大神的水平还是非常高的,方法和命名上面其实都是见名知意的。还有就是这块公平锁(FairSync)、非公平锁(NonfairSync) 都是依赖 AbstractQueuedSynchronizer 这个模板方法实现的。
lock()
是加锁方法,默认为非公平锁。
构造方法如下所示:
lock 方法如下所示:
公平锁 lock 方法实现
非公平锁 lock 方法实现
特别说明一下:下面会涉及到非公平/公平锁的解析,在解析之前我会加粗说是那种方式的锁。
acquire()
acquire 方法在 AbstractQueuedSynchronizer 类中,该方法就是去尝试获取锁,如果获取失败了,就会尝试再次获取,或者进入 AQS 队列中。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
主要有三条流程(本质就是 3 个核心方法) tryAcquire、 addWaiter、acquireQueued
FairSync#tryAcquire
1、公平锁tryAcquire 是由子类 FairSync 实现, 这里和非公平锁的区别就在于调用了 hasQueuedPredecessors 方法判断是否有线程在排队。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (
// hasQueuedPredecessors 这里就是公平锁的体现,如果人在排队就不能获取锁,需要先进入排队,所以性能上来说比非公平锁要一些。
!hasQueuedPredecessors() &&
// cas 获取锁
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;
}
2、addWaiter 操作,顾名思义就是当前线程尝试获取锁失败,然后进入将当前 Thread 封装为一个 AQS Node 节点,进入等待队列排队
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;
}
如果没有尾节点会调用 enq进行初始化头节点和入队操作:
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;
}
}
}
}
3、调用acquireQueued 方法是在,当前获取锁的线程进入 AQS 之后进入阻塞之前执行的方法,其实就是在进入等待之前做最后一次 “抢救” 尝试能否获取锁。还有一个逻辑就是在 AQS 唤醒过后,当前获取到锁的节点就成了头节点,会将 "旧" 头节点丢弃掉。
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);
}
}
NonfairSync#tryAcquire()
1、 本次走非公平锁 NonfairSync, 非公平锁对比公平锁没有是否有队列的排队的判断,只要是锁获取的参与者,都可以直接进行锁的竞争。
// 首先我们先看 `tryAcquire()` 方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 核心是调用 `nonfairTryAcquire(acquires)`
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果可以获取锁
if (c == 0) {
// 修改 state
if (compareAndSetState(0, acquires)) {
// 设置锁的持有者(持有线程)
setExclusiveOwnerThread(current);
return true;
}
}
// 重入支持
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2、 addWaiter(Node.EXCLUSIVE)
addWaiter方法实现
如果前面没有排队线程将调用 enq方法
双向链表中,第一个节点为虚节点(也叫做哨兵节点),其实并不存储任何信息,只是占位,真正的第一个有数据的节点,是从第二个节点开始的。
acquireQueued(addWaiter(Node.EXCLUSIVE))
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;
}
// shouldParkAfterFailedAcquire 第 1 次返回 false
// 第 2 次返回 false
if (shouldParkAfterFailedAcquire(p, node) &&
// 内部会阻塞线程调用 LockSupport.park(this);
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// setHead 方法
// 就是把当前获取锁的节点设置为哨兵节点,然后之前旧哨兵节点设置为 null
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
node.predecessor(); 方法获取前节点,之前我们有看到 aqs 是必须要初始化的,通常情况下都不是 null 的。
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
shouldParkAfterFailedAcquire()
shouldParkAfterFailedAcquire(p, node) 方法, 简单的讲就是把[node] 的有效前驱(有效是指node不是CANCELLED的)找到,并且将有效前驱的状态设置为SIGNAL,之后便返回true代表马上可以阻塞了。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱节点的状态
int ws = pred.waitStatus;
// 如果是 SIGNAL 状态,即等待被占用的资源释放,直接返回 true
// 准备继续调用 parkAndCheckInterrupt 方法
if (ws == Node.SIGNAL)
return true;
// ws 大于 0 说明是 cancelled 状态
if (ws > 0) {
// 循环判断节点的前驱节点是否也是 cancelled 状态,忽略该节点重新链接队列
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将当前节点的前驱节点设置为 SIGNAL 状态,用于后续唤醒操作
// 程序第一次执行到这里返回为 false,还会进行外层第二次循环,最终从代码第 7 行返回
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt(),
parkAndCheckInterrupt()阻塞当前线程, 就是当线程多次获取锁失败后,进入 AQS 阻塞队列中进入阻塞等待,等持有者释放资源,激活当前线程。
private final boolean parkAndCheckInterrupt() {
// 线程挂起,程序不会继续向下珍惜i给你
LockSupport.park(this);
// 根据 park 方法 api 描述,程序下面三情况会继续向下执行
// 1. 调用 unpark
// 2. 被中断 (interrupt)
// 3. 其他不合逻辑的返回才会继续向下执行
// 因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态
// 如果由于被中断,该方法会返回 true
return Thread.interrupted();
}
unlock 方法
非公平锁方式解锁
解锁过程:
1、 unlock 方法源代码入,内部调用 release 方法并且传入一个 1 ,其实可以理解为归还/释放一个这把锁。
2、release 方法中有两个操作:第一步是执行 tyRelease 方法进行真正的解锁,如果解锁失败返回 false 返回, 第二部如果解锁成功,会去判断 AQS 中是否有等待的节点(即等待的线程)如果有就调用 unparkSuccessor 去 unpark 队列中第一个等待的线程。
3、我们先来看 tryRelease 方法, 首先是获取 state 变量的值,然后通过
getState() - releases 来获取解锁后的值,再此之前还有一个判断就是判断的当前锁的持有者是否是当前线程,如果不是将抛出:IllegalMonitorStateException 异常;然后解锁后 state == 0 这个状态我们可以理解为 “完全解锁“ (其实这个是相对于锁重入来说的)。如果完全解锁就清空当前锁的线程持有者。然后在通过 cas 修改 state 的值。最终返回接锁后的布尔值。 其实这里有个小细节就是只有在 state = 0 的时候解锁的布尔值才会返回 true.
protected final boolean tryRelease(int releases) {
// state -1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
// 清空锁持有者
setExclusiveOwnerThread(null);
}
// cas 修改 state
setState(c);
return free;
}
4、unparkSuccessor(h);方法主要是实现 AQS 中节点的唤醒操作, 它接受的一个参数就是我们在第二个步骤的时候传入的 node 节点,其实就是 AQS 的对头节点。这里通常就在头节点的下一个节点 , 如果没有找到会通过尾节点向前查找。最终确定需要唤醒的排队节点 s 执行 LockSupport.unpark(s.thread) 方法进入前面的抢锁的逻辑。
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);
}
ReentrantLock 总结
流程图梳理:
其实我刚开始看这块的时候,还事比较乱的,大家可以按照自己的思路,结合
ReentrantLock 源码尝试去分析,才能逐步的清晰和掌握。