1. JUC源码分析系列--ReentrantLock 可重入锁
发布于 2022年 01月 26日 09:15
JUC(java.util.concurrent) 包在 JAVA 的并发编程中占领着绝对的地位,在各大开源框架中均能看到它的身影。最近看完了 JUC 的源码,做一个持续的输出,从并发工具到并发容器再到线程池进行分析。在浏览过程中,建议你打开对应的源码,跟着本文一起分析。(文末补上了完整的流程图)
开篇问题:
- 为什么一个 .lock() 方法就可以锁住一段同步代码块?(本文核心)
- 可重锁入是怎么实现的?
- 公平锁和非公平锁的区别在哪里?
- 获取锁的超时和中断是怎么实现的?
锁的实现过程
首先看一下 ReentrantLock 的几个方法:
private final Sync sync;
public ReentrantLock() {sync = new NonfairSync();}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
public void lock() {sync.lock();}
public void unlock() {sync.release(1);}
我们可以发现 ReentrantLock 的加锁、解锁方法都是通过调用其内部的 Sync 实现的,并且 Sync 是其内部抽象类,其实现分别有 FairSync 和 NonfairSync,分别代表了公平锁和非公平锁的实现。默认会使用非公平锁。我们看一下 Sync 的类图。上面的父类就是大名鼎鼎的 AQS ,我会在接下来的文章里分析。
FairSync # lock() :
final void lock() {
acquire(1);
}
// 发现其调用的是 AQS 中的 acquire 方法,深入看一下
AbstractQueuedSynchronizer # acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们可以发现 acquire() 这里判断分成了两步,大致的意思就是先尝试能不能获取到锁,如果不能的话就进入到等待的线程中排队去,分别对应的就是 tryAcquire(arg) 和 acquireQueued(addWaiter(~)),acquireQueued 这个方法是 AQS 排队的实现,我会放到 # 中来讲,所以我们这里就可以理解为先用 tryAcquire() 方法尝试一下能不能拿到锁,拿到的时候会返回 true,这个方法就直接返回了。如果拿不到锁了,就到 AQS 中排队等着去吧。
我们这里重点看一下 tryAcquire() 的实现,我们会发现这是 AQS 提供的一个模板方法,其实现我们可以在 FairSync 中找到:
FairSync # tryAcquire() :
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取 AQS 中的 state 字段,在 ReentrantLock 中,state == 0 代表锁处于空闲状态,state > 0 代表锁已经被某个线程获取了
int c = getState();
if (c == 0) {
// 首先判断 AQS 的是否有其他线程在排队,对于公平锁来说是要讲究先来后到的
if (!hasQueuedPredecessors() &&
// 使用 CAS 来设置 state
compareAndSetState(0, acquires)) {
// 设置当前线程为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
// 判断当前线程是否为锁的持有线程,如果是的话代表重入了
else if (current == getExclusiveOwnerThread()) {
// 从这里可以看出 state 代表了锁重入的次数
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 由于 state 是大于 1 的,其他线程是没法到达这一步的,所以不需要 CAS 就可以直接设置 state
setState(nextc);
return true;
}
return false;
}
我们这里稍微分析一下 CAS。compareAndSetState(0, acquires)
这行代码使用了 Unsafe
类对 AQS 的变量 state
进行修改,这个修改是直接发生在本地内存上的。首先 Unsafe
会查找到 state
在本地内存的位置,把它跟 0 做对比,相等的话在更新。这里看起来是分两步执行(先对比后更新),实际上底层是由一个CPU命令完成的,所以还是原子操作的。并且在执行这个命令前会对总线加锁,让其他处理器暂时不能访问内存。这样子就保证了对 state
更新的安全性。
从多线程的角度分析这段代码如何防止并发,我们可以发现线程想要获得锁,即这个函数返回 true,都是需要经过 compareAndSetState(0, acquires)
这个 CAS 操作的。当一个线程拿到锁后,其他线程再进行 CAS 的时候就会发现本地内存上的 state
已经不是 0 了,所以都会返回失败。而且 CAS 又保证了原子性,我们可以看成各个线程之间的 compareAndSetState(0, acquires)
都是顺序执行的,不存在并发的情况。
看完了加锁的过程,我们再看一下解锁是怎么实现的。
ReentrantLock # unlock
public void unlock() {
// 直接调用了 AQS 的 release
sync.release(1);
}
AbstractQueuedSynchronizer # release
public final boolean release(int arg) {
// 解锁的模板方法,在ReentrantLock的内部类实现
if (tryRelease(arg)) {
// 留到下节分析,实现了唤醒队列中排着队的其他线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ReentrantLock # Sync # tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 不为 0 的时候说明锁重入了,本线程还持有着这个锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
锁的释放的实现比较简单,直接更新 state
和 exclusiveOwnerThread
,也不需要考虑并发,因为在这两个更新前,其他线程对 state
的更新都会失败。至此对 ReentrantLock
加锁解锁的分析就结束了,其实看完源码会发现 ReentrantLock
就是通过对一个变量进行 CAS 操作,成功就是拿到锁了,不成功就去排队。至于如何排队,就是 AQS 为所有并发工具类提供的功能了。
解答开篇
可重入锁是怎么实现的
其实通过刚才对加锁过程的分析我们已经可以得到可重入锁就是当锁的持有对象就是本线程的时候,就对 state
的累加。不过由于解锁操作只是对 state
减一,所以当使用重入锁时,还是要记得调用对应次数的解锁操作。
公平锁和非公平锁的区别在哪里
刚才我们是拿 FairSync
也就是公平锁进行分析的,我门再来看一下 NonfairSync
的加锁是怎么进行的。
NonfairSync#lock()
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
我们可以发现非公平锁就是在竞争锁前先尝试了一次CAS操作(就是插队的感觉),另外就是在tryAcquire
中,把对列是否存在排队线程的判断去掉了。这样即使在本线程前存在多个排队队列,本线程也是有机会率先拿到锁的。
获取锁的超时和中断是怎么实现的?
ReentrantLock
为我们提供了一个通过指定时间来获取锁的方法,当时间到了并且还是没有取到锁的话就会返回一个 false
。
ReentrantLock#tryLock
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
Sync#tryAcquireNanos
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取锁,如果不成功的话就调用 AQS 支持的时间排队法了
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
我们可以发现超时机制的支持主要还是靠 AQS 的doAcquireNanos(arg, nanosTimeout)
实现的。这个方法我会在下一篇里面分析,这里我们只需要知道它也是同样的把线程放到队列中去排队,当达到超时时间时,就会返回一个 false。
总结
今天我们分析了 ReentrantLock
的源码,知道的加锁解锁过程其实就是在竞争对 state
变量的修改,对于没有竞争到锁的线程,则通过 AQS 提供的排队能力进入到队列中等待。此外ReentrantLock
还提供了对其相关变量的 get 方法,比如获取锁的持有线程,获取state
的值,获取整个排队队列等,建议你通过源码查看这些方法。