一 API 阅读
一种可重入的互斥锁。拥有和 synchronized 关键字相同的功能,除此之外,也有一定的功能扩展。
一个 ReentrantLock 锁会被成功调用了 lock 方法,且还没有 unlock 的线程持有。检查一个线程是否持有锁的方法是 isHeldByCurrentThread 和 getHoldCount。
构造函数可以包含一个可选的 boolean 值,表示构建的锁是一个 公平锁 还是
非公平锁。使用默认的无参构造时,这个参数默认为 false 即非公平锁。当入参为 true 的时候,表示这是一个公平锁,排队的队列里等待最久的线程最先获得锁。传入参数为 false 的时候表示这是一个非公平锁,不会遵循公平锁里线程获取锁的策略。在竞争线程较多的情况下,使用公平锁会导致较低的吞入量。
需要注意的是,不定期地调用 tryLock 方法,会让争用线程不遵循公平锁的竞争模式。当恰巧锁资源被释放,而还有排队线程的时候,主动调用方法可能会成功提前获取到锁。
使用 ReentrantLock 的常见惯例如下
class X {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock();
try {
// ... method body
} finally {
lock.unlock();
}
}
}
作为一个可重入锁,ReentrantLock 允许同一个线程的重入次数为 Integer.MAX_VALUE。
二 部分代码阅读
看这部分的代码的时候,需要结合前面的文章 AQS 部分一起来看。
2.1 非公平锁的 lock 流程
private final Sync sync;
需要注意,这里这个成员变量 sync 是 reentrantLock 实现同步机制的核心类。因为 reentrantLock 使用的是 AQS 同步框架,而 sync 就是这个 AQS 的内部实现类。
这里 sync 的实际实现,在 reentrantLock 里面分成了两大类。一个是公平锁实现,另一个是非公平锁实现。这里的编码 遵循了单一职责原则,也符合 AQS 同步器框架的推荐做法。
当我们使用默认的无参构造函数创建一个 reentrantLock 实例。然后调用 lock() 方法,其流程如下:
实际调用的方法就是这里的
java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
final void lock() {
// cas 方式更新 AQS 的 state 成员值 +1
if (compareAndSetState(0, 1))
// 更新成功,设置独占锁线程引用为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// cas 更新失败,调用 AQS 的 acquire 方法
acquire(1);
}
先尝试直接修改 AQS 内部维护的 state 成员变量,0 表示没有线程持有锁,由 CAS 方式更新为 1。如果更新成功,即表示当前线程成功持有了这个可重入独占锁,这时更新一下独占锁的线程引用为当前线程。
如果 cas 方式更新 state 字段失败,那么就调用 AQS 内定义的 acquire 方法来尝试获取锁。这个方法之前在 AQS 源码阅读的时候详细读过。通过定义一套模板方法,来实现加锁操作。其中的方法
- acquireQueued
- addWaiter
都是 AQS 自己实现,子类需要补充的方法是
- tryAcquire
在内部类
java.util.concurrent.locks.ReentrantLock.NonfairSync
中,这个方法的实现指向了
java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire 代码如下:
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取 AQS 内成员变量 state
int c = getState();
// 如果 state 为 0,表示锁空闲,尝试获取锁
if (c == 0) {
// cas 方式更新 state 字段
if (compareAndSetState(0, acquires)) {
// 更新成功,设置当前线程引用为
setExclusiveOwnerThread(current);
return true;
}
}
// state 不为 0,表示锁已经被某线程持有,先检查是不是自己持有
else if (current == getExclusiveOwnerThread()) {
// ReentrantLock 支持重入,所以累加 acquire 值
int nextc = c + acquires;
// 检查重入次数有没有溢出,溢出则抛出异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 未溢出,更新 state 值
setState(nextc);
return true;
}
// 获取锁失败,返回false
return false;
}
todo nonfairTryAcquire 流程图
如果此处的 nonfairTryAcquire 方法加锁失败,那么尝试加锁的线程会被加入同步队列排队(即 AQS 的 addWaiter 和 acquireQueued 方法)。而这个同步队列的排队唤醒线程机制又是默认的 非公平锁 机制。
至此,我们应该知道的是,reentrantLock 的非公平锁核心机制是依赖于 AQS 的内容实现的。reentrantLock 本身也没有维护线程等待队列,这是 AQS 的工作。reentrantLock 只是通过内部类来实现了这个功能。
2.2 公平锁的 lock 流程
当以如下的方式声明一个 reentrantLock 对象时,我们就可以得到一个公平锁。
ReentrantLock lock = new ReentrantLock(Boolean.TRUE);
公平锁和非公平锁的区别在于:排队线程的获取锁时机是有顺序的,等待最久的线程最先获得锁。
与默认的 NoFairSync 实现相比,其他的都一样,主要的区别在自己实现的 tryAcquire 方法。
java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire
// 公平锁版本的 tryAcquire
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取 AQS 同步器维护的锁状态字段 state
int c = getState();
// c == 0 表示当前锁处于空闲状态,可以尝试获取锁
if (c == 0) {
// hasQueuedPredecessors 方法用于判断当前尝试获取锁的线程是否需要排队,如果不需要排队则直接更新 state 字段并设置独占线程的引用,在判断体内返回 true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 走到这里 c != 0,即锁已被占有,检查持有锁的线程是不是当前线程自己
else if (current == getExclusiveOwnerThread()) {
// 是当前线程持有锁,增加重入加锁次数,传入的 acquires 为 1
int nextc = c + acquires;
// 重入次数超过 Integer.MAX_VALUE 溢出为负数,抛出异常
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 未溢出,设置更新 state 字段值
setState(nextc);
// 返回 true
return true;
}
// 尝试加锁失败,返回 false
return false;
}
}
公平锁与非公平锁的 tryAcquire 方法,主要区别在一个地方
hasQueuedPredecessors
当锁处于空闲状态时,公平锁加锁的前置判断条件多了这么一个方法。
在 state = 0 的条件下,非公平锁内的线程不用检查 AQS 维护点队列信息而直接尝试争用锁;
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 维护的线程等待队列的头节点/尾节点引用。然后有一个嵌套的判断逻辑,返回 false 表示可以直接加锁,返回 true 的时候就需要入队。
第一个条件 A,头节点不等于尾节点,即队列中还有在排队的线程。如果这个条件不满足(即头节点等于尾节点),说明队列中无排队线程,可以直接入队,不需要将现有线程入队。此时触发短路逻辑,直接返回 false。
第二个条件组 B,两个条件满足一个即可
- 头节点的后继节点不为空
- 头节点的后继节点不是当前尝试获取锁的节点,如果这条为 false,表示排队里下一个即将拿到锁的线程就是当前线程
在条件 A 返回 true 的情况下:
当这两个判断 B1,B2 同时为 false,表示同步队列有排队线程,并且同步队列里排队最靠前都线程就是当前线程,这个时候也就 不需要排队, 直接获取。
B1 返回 true,这个时候同步队列正处在初始化过程中,此时触发了条件组 B 的短路逻辑。整个条件组 B 返回 true。说明已经有其他线程在当前线程之前争用锁了,那么当前线程 需要排队 。整个判断逻辑返回 false。
B1 返回 false,B2 返回 true。表示同步队列正在初始化过程中,并且排队等待的下一个线程不是当前线程,那当前线程依旧需要 加入排队队列 等候。
2.3 unlock 流程
公平锁和非公平锁的释放锁流程都是一样的。当我们调用
reentrantLock.unlock()
方法,debug 源代码,可以看到还是使用了实现了 AQS 内部类的成员变量的释放锁方法。
public void unlock() {
sync.release(1);
}
而对应的 release 方法的代码如下,这个模板方法依然是在 AQS 同步器内。
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
// 获取头节点
Node h = head;
// 头节点不为空且头节点的节点状态不为0(不为0表示这个节点不是初始化虚拟节点)
if (h != null && h.waitStatus != 0)
// 修改节点 status 字段并唤醒等待线程
unparkSuccessor(h);
return true;
}
// 释放锁失败,返回 false
return false;
}
tryRelease 方法和之前的 tryAcquire 方法一下,都是需要 AQS 同步器的实现类自己编写的部分。
java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
// 内部类实现的 - 尝试释放锁方法,注意传入的 releases 值为 1
protected final boolean tryRelease(int releases) {
// 获取当前 state 值,然后减 1,得到一个释放锁之后 state 的期望值
int c = getState() - releases;
// 检查释放锁线程和加锁线程是不是同一个线程
if (Thread.currentThread() != getExclusiveOwnerThread())
// 不是的话,直接抛出异常
throw new IllegalMonitorStateException();
boolean free = false;
// 如果 state 期望值为 0,表示没有重入加锁,现在可以直接释放锁
if (c == 0) {
// 注意只有当 state 计数值为 0 的时候,才能释放锁,否则表示之前同一个线程有重入加锁操作
free = true;
// 取消独占线程的引用
setExclusiveOwnerThread(null);
}
// 更新 state 值
setState(c);
// 返回释放锁标识位
return free;
}