本文会介绍 ReentrantLock 的核心源码以及常见问题,过程中会涉及到一点点 AQS 的部分,但是只会做简单的介绍。建议在读本文时不要发散开去学 AQS,相关内容后续会专门梳理一遍。
[riː’entrəntlɒk]
开始阅读源码之前,一定要先从整体上看一下类图,本文的重点就是标颜色的模块。
- ReentrantLock 的核心是内部类 Sync。
- Sync 继承于 AQS,AQS 又继承于 AbstractOwnableSynchronizer。
- Sync 有两个子类,分别实现了公平锁和非公平锁。
首先我们同步一下认知:
可重入锁是一个概念,ReentrantLock 是满足这个条件的具体实现,并不是说只有 ReentrantLock 才是可重入锁。我是个男人,但男人不仅仅只有我一个。
可重入锁,指的是【同一个线程】可以对【同一个资源】重复地加锁,而不需要经历先释放再加锁的操作。先有个概念就够了,通过下文的源码理解我们再去了解可重入具体的操作指的是什么。
既然有可重入锁,那必然有对应的不可重入锁,此外还有各种乱七八糟的概念如偏向锁、内置锁、乐观锁等等。这些都不是互斥的概念,都是不同场景下满足某一个特征的叫法,千万不要在此展开,本文我们就只关注可重入。
上面刚说了先不展开其他的锁,怎么突然又来了一对新概念?
因为 ReentrantLock 的核心内部类是 Sync,而 Sync 对应两个不同的实现类,也就是公平锁与非公平锁,不讲不行。
从字面意思上看,公平锁就是公平的,非公平锁就是不公平的,真的是一句酣畅淋漓的废话哈哈。
公平,体现在当前线程去尝试拿锁的时候,是否需要关注其他线程的优先级更高。如果是公平锁,则需要礼让其他优先级更高的线程,非公平锁则不在意其他线程,一有机会就拿锁,相当于插队。此外,关于怎么定义这个优先级,下文也会简单提一下。
这么看起来,非公平锁是不正义的,而正义,只在射程之内!
Sync 类继承的父类,也就是卷死的 AQS 的全称,在 ReentrantLock 的实现中就大量依赖了 AQS 提供的能力所以简单提一。
AQS 值得单独写一篇文章,本文不会深入 AQS 的细节,先简单理解为它提供了一个线程排队的队列,利用这个队列先进先出的特性来实现所谓的公平。
AQS 又继承于 AbstractOwnableSynchronizer,简单看一下源码其实很短,它的作用就是包含了持有了当前的锁的线程信息。
ReentrantLock 核心变量就一个 Sync 类型的对象,也就是说对应的能力全部基于 sync 对象展开,继续往下看。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
// ReentrantLock 持有的是一个 Sync 类型的变量
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
// 省略
}
}
ReentrantLock 有两个构造方法,他们的区别在于 Sync 对象的实现类,一个是 NonfairSync 对象,另一个则根据入参决定,为 true 时对应 FailSync 对象,false 时对应 NonfairSync 对象。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
Sync 是 ReentrantLock 中定义的一个内部类,它的子类分为公平实现和非公平实现,使用 AQS 状态来获取锁的保持次数。这里先不用关注具体的实现逻辑,瞄一眼就继续往下看吧,方法的大概功能先都写在注释上了。
/**
* 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;
abstract void lock();
// 以非公平的方式获取锁
final boolean nonfairTryAcquire(int acquires) {
// 先忽略
}
// 释放锁
protected final boolean tryRelease(int releases) {
// 先忽略
}
// 判断当前是否为锁的持有者
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 获取当前锁的持有线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 获取锁的持有次数
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
// 判断当前是否处于上锁状态
final boolean isLocked() {
return getState() != 0;
}
}
公平锁子类
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
}
非公平锁子类
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() {
// 非公平锁的 lock 方法中,直接就开始通过 CAS 的方式获取锁(就是setState) // 如果上锁成功,则直接设置当前线程为持有者了。
// 如果上锁失败,才会调用 acquire() 方法。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
仔细对比一下公平锁 FairSync 和非公平锁 NonfairSync 这两者中 lock() 的实现,非公平锁的实现中一开始就尝试 CAS 获取锁,完全没有对当前的锁状态和其他线程进行判断,这就是为什么叫非公平的原因,我来了就要拿锁,我不管别人怎么样。
而公平锁没有这么做,而是直接调用 acquire() 方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire 方法在公平锁 FairSync 和非公平锁 NonfairSync 的子类中都进行了重写,
对比一下差异其实就在 if (c=0) 的下一行中,即是否判断队列中有等待的线程。
公平锁中的实现
/**
* Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果当前处于无锁状态
if (c == 0) {
// 关键:如果队列中有其他线程排在当前线程之前,则判断
// hasQueuedPredecessors()=true,注意前面还有一个 !,此外因为
// 短路机制所以后面的 compareAndSetState 也不会执行。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 此时处于有锁状态,判断当前的持有者如果已经是当前线程的话,则累加 state 值
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
非公平锁中的实现
/**
* Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键:对比于公平锁,这里没有判断等待队列中是否有其他线程排在前面,
// 直接通过 CAS 尝试拿锁,如果 CAS 操作成功则当前线程直接就截胡拿到锁了。
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;
}
上面已经讲了公平锁和非公平锁分别对 lock() 方法的实现,我们再来看下 tryLock 。tryLock 也有两个不同的实现,先看没有参数的这个
public boolean tryLock() {
// 直接调用 nonfairTryAcquire 尝试拿锁,上文已经分析过这个方法,是非公平的。
return sync.nonfairTryAcquire(1);
}
在来看带超时时间的 tryLock(),首先要注意,这个方法会抛出 InterruptedException 异常。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
// 直接调用 tryAcquireNanos 方法。
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 先判断线程是否中断,是的话直接抛异常了
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquire 尝试拿锁,上面讲过了。重点看下 doAcquireNanos 方法
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos 的实现在 AbstractQueuedSynchronizer 类中,复习一下,这是个线程等待队列,简单概括一下就是
- 在超时时间内循环得去判断当前等待队列中的其他线程,如果轮到当前线程了则尝试加锁。
- 在这个过程中去判断超时时间以及线程是否被中断,如果超时则返回,中断则抛异常。
- 其他详情写在注释了。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
// 在队列尾部插入一个等待的节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
// 获取当前节点 node 的前驱节点 p
final Node p = node.predecessor();
// 如果已经是头节点了,则尝试获取锁,获取成功则世界返回 true
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 过程中判断超时时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
// shouldParkAfterFailedAcquire 先简单理解为将当前节点的
// 前驱节点设置一个标记,如果轮到它了则触发通知。
// 此外,如果剩余超时时间大于 spinForTimeoutThreshold(常量,1000纳秒) ,则直接让当前线程进入阻塞休眠。如果小于这个时间了,则继续开始尝试拿锁过程。
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
// 最后别忘了,如果尝试拿锁失败了,则将当前的 node 节点删除。
if (failed)
cancelAcquire(node);
}
}
unlocl 就是释放当前线程锁的过程,概括一下
- 调用 tryRelease 方法对 state 值进行递减,如果减到 0 则说明当前线程对锁的持有已经完全释放。
- 当前线程释放锁之后,判断等待队列中是否还有其他在排队的线程,有的话则唤醒下一个节点。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 直接调用 tryRelease 方法
if (tryRelease(arg)) {
// 当 tryRelease 返回 true,即释放锁成功,此时再判断等待队列中是否还有
// 等待中的线程节点,有的话则调用 unparkSuccessor 唤醒下一个排队中的线程。
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 对 state 值进行递减
int c = getState() - releases;
// 判断当前线程是否为资源的持有者,不是的话当然不能释放锁,直接抛异常。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 当 c 的值递减到 0,则说明锁已经释放,返回 free=true 并设置资源的当前持有者
// 为 null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
至此,关于加锁与释放锁的代码流程已经讲完了,当时我有一个困惑,所谓的加锁操作到底是什么意思?又是在哪里体现了重入呢?加锁和释放的代码中一直在更新一个 state 值,它又是什么?
我们再来瞄一眼加锁和释放锁的过程,从始至终都是对 state 这个值的读写。
final void lock() {
// CAS 更新 state=1
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取 state 的值
int c = getState();
if (c == 0) {
// 忽略
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
state 字段是 AQS 类 AbstractQueuedSynchronizer 中的一个字段,注意使用了 volatile 关键字修饰。
加锁和释放锁的操作本质上就是对 state 字段的赋值,加锁时执行 +1,释放锁时执行 -1。
当 state=0 时标志当前资源处于无锁状态,state=1 则表示有一个线程正在持有,而 state > 1 时则表示同一个线程多次加锁成功,这也就是重入的本质。
/**
* The synchronization state. */
private volatile int state;
getHoldCount() 方法实质上就是获取了 state 的值,注意限制了当前线程只能获取自身的持有锁的次数,若不是当前线程则直接返回 。
public int getHoldCount() {
return sync.getHoldCount();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
isLocked() 方法 本质上就是判断 state 值是否为 0
public boolean isLocked() {
return sync.isLocked();
}
final boolean isLocked() {
return getState() != 0;
}
ReentrantLock 继承 AQS ,利用的是 AQS 提供的线程队列,当多个线程在竞争的时候会进入此队列中排队,通过这个队列辅助锁的分配。
AQS 中的关键字段也使用 volatile 字段修饰,以此来保证多线程竞争时的可见性。