一、基本概念
ReentrantLock 是 Java 中提供的一个可重入互斥锁,它是 java.util.concurrent.locks 包中的一个接口 Lock 的实现类。ReentrantLock 提供了比使用 synchronized 关键字更强大的锁定机制,例如 公平锁 和 非公平锁 选择、尝试锁定、可中断锁定等。
ReentrantLock 主要有以下几个特点:
- 可重入性:允许同一个线程多次获取同一个锁。
- 公平性:可以选择是否按照请求锁的顺序来获取锁。
- 扩展性:提供了多种锁操作,如 tryLock 和 unlock 方法。
- 中断响应:支持响应中断信号,使线程可以在等待锁时响应中断。
可重入性
可重入性是指一个线程可以多次获取同一个锁的能力。在 Java 中,synchronized 关键字也支持可重入性,但 ReentrantLock 提供了更灵活的方式来实现这一特性。
在 ReentrantLock 的内部实现中,可重入性是通过 AbstractQueuedSynchronizer(AQS)中的 state 字段来跟踪的。state 字段是一个 volatile 的整型变量,用于记录锁的持有状态。
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a {@code volatile} read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that the actual
* value was not equal to the expected value.
*/
// 以CAS原子方式更新state值
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
- 获取锁:当线程尝试获取锁时,如果该线程已经拥有锁,那么
state
的值会增加。这表示该线程可以再次获取锁。 - 释放锁:当线程释放锁时,
state
的值会减少。只有当state
的值变为零时,锁才会被完全释放,并允许其他线程获取锁。
举个例子:
抓娃娃机,玩家需要投入游戏币才能使用。每次投入一枚游戏币,玩家就可以尝试抓取一次娃娃。现在,我们将这台抓娃娃机类比为一个可重入锁(ReentrantLock
),其中娃娃机的当前投币数可以被视为锁的状态计数器(state
)。
可重入锁的状态(state
)
- 当
state
为 0 时,表示当前娃娃机没有人在使用(锁未被任何线程持有)。 - 当
state
大于 0 时,表示娃娃机正在被某位玩家使用,并且state
的值表示该玩家投入的游戏币数量(即该玩家已经获取锁的次数)。
玩家使用娃娃机的过程
- 小明投币使用:小明可以一次性投入多枚游戏币,这意味着他可以连续抓取多次娃娃(可重入性,一个线程可以多次获取同一把锁)。
- 抓取娃娃:每当小明抓取一次娃娃,
state
的值减少 1,表示小明使用了一枚游戏币。 - 抓取结束:只有当
state
的值回到 0 时,表示小明已经使用完了所有投入的游戏币,这时娃娃机才可供下一位玩家使用(锁被完全释放,允许其他线程获取锁)。 state 不为0时,其他玩家应当等待小明玩完才能够使用(阻塞)
公平锁
公平锁遵循先进先出的原则,即按照线程请求锁的顺序来获取锁。这意味着如果一个线程在另一个线程之前请求了锁,那么它将在那个线程之前获取到锁。
公平锁的实现主要是通过 AbstractQueuedSynchronizer 中的 hasQueuedPredecessors() 方法来判断是否有其他线程排在当前线程前面。如果存在这样的线程,那么当前线程就会等待,直到前面的线程释放锁。
举个例子:
还是抓娃娃,当 state 不为0时,即小明正在使用这台娃娃机,其他玩家依次在后面排队等待,当小明用完所有的币时,队首的人可以使用这台娃娃机。
Reentrantlock FairSync类中的实现:
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步器的内部状态值。
int c = getState();
if (c == 0) {
// 检查是否有其他线程在队列中等待
if (!hasQueuedPredecessors() && // 确保当前线程是第一个等待的线程
compareAndSetState(0, acquires)) { // 尝试设置 state 为 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;
}
-
AbstractQueuedSynchronizer 类继承的方法,用于获取同步器的内部状态值。该值用来判断锁是否已经被其他线程持有int c = getState();
-
// 检查是否有其他线程在队列中等待 if (!hasQueuedPredecessors() && // 确保当前线程是第一个等待的线程 compareAndSetState(0, acquires)) { // 尝试设置 state 为 acquires setExclusiveOwnerThread(current); return true; }
!hasQueuedPredecessors(): 检查当前线程前面是否有等待获取锁的线程。公平锁要求所有线程排队等待获取锁,所以只有排在队首的线程才能尝试获取锁;
compareAndSetState(0, acquires): 尝试将锁的状态从0设置为acquires;
setExclusiveOwnerThread(current): 如果当前线程前面没有等待获取锁的进程,并且锁状态值为0,则设置当前线程为锁的独占所有者。
-
// 重入锁处理 else if (current == getExclusiveOwnerThread()) { // 更新锁状态值 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; }
如果当前线程是锁的持有者,则根据重入值更新state。
非公平锁
非公平锁不保证线程获取锁的顺序。在非公平模式下,即使有其他线程正在等待锁,当前线程仍然可以直接尝试获取锁。这种模式可能会导致饥饿现象,但通常具有更好的吞吐量。
非公平锁的实现中,当前线程在尝试获取锁时不会检查是否有其他线程在等待队列中。这意味着当前线程总是会尝试获取锁,无论是否有其他线程正在等待。
举个例子:
其他玩家不会排队等待这台娃娃机,而是当小明用完所有的币时,所有玩家都会尝试来使用这台娃娃机,每个人都有机会进行使用,而最先抢到的拥有使用权。
Reentrantlock NonFairSync类中的实现:
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
与公平锁类似,差别在于非公平锁不会判断当前线程前是否有其他线程在尝试获取锁,而是一旦compareAndSetState(0, acquires)成功,就会设置当前线程为锁的持有者。
二、ReentrantLock执行流程
递归计算斐波那契数列:
private final ReentrantLock lock = new ReentrantLock();
public long fibonacci(long n) {
lock.lock();
try {
if (n <= 1) {
return n;
} else {
// 递归调用时,由于同一个线程持有锁,不会发生阻塞
return fibonacci(n - 1) + fibonacci(n - 2);
}
} finally {
lock.unlock();
}
}
首先看构造函数,ReentrantLock默认使用非公平锁:
/**
* 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(); // 可以传入boolean参数决定使用哪种锁
}
加锁 lock.lock()
执行lock.lock();方法时,尝试加锁:
public void lock() {
sync.lock();
}
- 公平锁
公平锁会尝试以排队方式(见上述公平锁tryAcquire方法)获取锁,如果获取失败,则将线程加入到等待队列中,接下来调用 acquireQueued 方法,该方法会阻塞当前线程,直到锁被释放,并有机会再次尝试获取锁。
// lock
final void lock() {
acquire(1);
}
// acquire
@ReservedStackAccess
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 非公平锁
非公平锁则是直接尝试获取锁,获取失败同样将线程加入到等待队列中并阻塞。
@ReservedStackAccess
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
解锁 lock.unlock()
// unlock入口方法
public void unlock() {
sync.release(1);
}
// release
@ReservedStackAccess
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(int arg):
- 这是一个内部方法,用于尝试释放锁。
- 它会减少 state 的值,表示锁的持有计数减一。
- 如果 state 的值变为零,那么锁被完全释放,并且当前线程不再是锁的所有者。
- 如果成功释放锁,返回 true;否则返回 false。
Node h = head:
- 获取等待队列的头部节点。头部节点是当前持有锁的线程对应的节点,或者是在锁被释放后准备尝试获取锁的线程对应的节点。
if (h != null && h.waitStatus != 0):
- 检查头部节点是否存在,以及它的 waitStatus 是否不为零。
- waitStatus 表示节点的状态,通常用来指示线程是否被阻塞。
- 如果 waitStatus 不为零,那么它可能处于阻塞状态。
- 如果头部节点存在并且它的 waitStatus 不为零,那么接下来将尝试唤醒它的后继节点。
unparkSuccessor(Node node):
- 唤醒头部节点的后继节点。