经历了AQS的前世和今生后,我们已经知道AQS是Java中提供同步状态原子管理,线程阻塞/唤醒,以及线程排队功能的同步器基础框架。那么我们今天就来学习通过AQS实现的ReentrantLock。按照惯例,先来看3道关于ReentrantLock的常见面试题:
接下来,我会尽可能的通过剖析源码的方式为大家解答以上的题目。
Tips:本文提供了省流版题解。
ReentrantLock译为可重入锁,在《一文看懂并发编程中的锁》中我们解释过锁的可重入特性:同一线程可以多次加锁,即可以重复进入被锁定的逻辑中。
Doug Lea是这样描述ReentrantLock的:
A reentrant mutual exclusion {@link Lock} with the same basic behavior and semantics as the implicit monitor lock accessed using {@code synchronized} methods and statements, but with extended capabilities.
“A reentrant mutual exclusion Lock”说明ReentrantLock除了具有可重入的特性,还是一把互斥锁。接着看后面的内容,ReentrantLock与使用synchronized方法/语句有相同的基本行为和语义。最后的" but with extended capabilities"则表明了ReentrantLock具有更好的拓展能力。
那么可重入互斥锁就是ReentrantLock的全部吗?别急,我们接着往后看:
The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order.
ReentrantLock提供了公平/非公平两种模式,默认非公平模式,可以通过构造器参数指定公平模式。
好了,目前为止我们已经对ReentrantLock有了比较清晰的认知了,按照《一文看懂并发编程中的锁》中的分类,ReentrantLock本质是互斥锁,具有可重入特性,此外ReentrantLock还实现了公平和非公平两种模式。
ReentrantLocak的使用非常简单:
ReentrantLock lock = new ReentrantLock();
lock.lock();
// 业务逻辑
lock.unlock();
通过无参构造器创建ReentrantLock对象后,调用lock和unlock进行加锁和解锁的操作。除了无参构造器外,ReentrantLock还提供了一个有参构造器:
// 无参构造器
public ReentrantLock() {
sync = new NonfairSync();
}
// 有参构造器
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
FairSync和NonfairSync是ReentrantLock的内部类,可以通过构造器来指定ReentrantLock的公平模式或非公平模式。具体实现我们先按下不表,先来看ReentrantLock中提供的其它方法。
除了常用的lock外,ReentrantLock还提供了3个加锁方法:
// 尝试获取锁
public boolean tryLock();
// 尝试获取锁,否则排队等候指定时间
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
// 尝试获取锁
public void lockInterruptibly() throws InterruptedException;
tryLock直接尝试获取锁,特点在于竞争失败时直接返回false,不会进入队列等待。重载方法tryLock(long timeout, TimeUnit unit)
增加的在队列中的最大等待时间,如果锁竞争失败,会加入到等待队列中,再次尝试获取锁,直到超时或中断。
lockInterruptibly的特点是,调用thread.interrupt中断线程后抛出InterruptedException异常,结束竞争。虽然lock也允许中断线程,但它并不会抛出异常。
除了常用的加锁方法外,ReentrantLock还提供了用于分析锁的方法:
方法声明 |
作用 |
|
返回当前线程持有锁的次数,即当前线程重入锁的次数 |
|
返回等待获取锁的线程数量估算值 |
|
查询当前线程是否在等待获取锁 |
|
是否有线程在等待获取锁 |
|
是否为公平锁 |
|
当前线程是否持有锁 |
|
锁是否被线程持有,即锁是否被使用 |
|
创建条件对象 |
|
等待在该条件上的线程数量 |
|
是否有线程在等待在该条件上 |
接下来,我们通过源码来分析ReentrantLock的公平/非公平模式,以及重入性的实现原理,并对比不同的加锁方法的实现差异。
我们先来来了解下ReentrantLock的结构:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
// 同步器
abstract static class Sync extends AbstractQueuedSynchronizer {}
// 非公平模式同步器
static final class NonfairSync extends Sync {}
// 公平模式同步器
static final class FairSync extends Sync {}
}
ReentrantLock仅仅实现了Lock接口,并没有直接继承AbstractQueuedSynchronizer,其内部类Sync继承AbstractQueuedSynchronizer,并提供了FairSync和NonfairSync两种实现,分别是公平锁和非公平锁。
我们已经知道,可以指定不同的参数来创建公平/非公平模式的ReentrantLock,反应到源码中是使用不同的Sync的实现类:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
并且在加锁/解锁操作中,均由Sync的实现类完成,ReentrantLock只是对Lock接口的实现:
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
先来回想下《AQS的今生,构建出JUC的基础》中的acquire方法:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
}
AQS自身仅实现了将线程添加到等待队列中的acquireQueued方法,而预留了获取锁的tryAcquire方法。
那么我们不难想到,ReentrantLock的作用机制:继承自AQS的Sync,实现了tryAcquire方法来获取锁,并借助AQS的acquireQueued实现排队的功能,而ReentrantLock的公平与否,与tryAcquire的实现方式是息息相关的。
FairSync非常简单,仅做了tryAcquire方法的实现:
static final class FairSync extends Sync {
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态,AQS实现
int c = getState();
// 判断同步状态
// c == 0时,表示没有线程持有锁
// c != 0时,表示有线程持有锁
if (c == 0) {
// hasQueuedPredecessors判断是否有已经在等待锁的线程
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 线程重入,同步状态+1
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
// 更新同步状态
setState(nextc);
return true;
}
return false;
}
}
当c == 0时,锁未被任何线程持有,通过hasQueuedPredecessors判断是否已经有等待锁的线程,如果没有正在等待的线程,则通过compareAndSetState(0, acquires)
尝试替换同步状态来获取锁;当c != 0
时,锁已经被线程持有,通过current == getExclusiveOwnerThread
判断是否为当前线程持有,如果是则认为是重入,执行int nextc = c + acquires
,更新同步状态setState(nextc)
,并返回成功。
FairSync的公平性体现在获取锁前先执行hasQueuedPredecessors,确认是否已经有线程在等待锁,如果有则tryAcquire执行失败,默默的执行AQS的acquireQueued加入等待队列中即可。
NonfairSync也只是做了tryAcquire的实现,而且还只是掉用了父类的nonfairTryAcquire方法:
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
@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) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
}
NonfairTryAcquire与FairSync#tryAcquire
简直是一模一样,忽略方法声明,唯一的差别就在于,当c == 0
时,nonfairTryAcquire并不会调用hasQueuedPredecessors确认是否有线程正在等待获取锁,而是直接通过compareAndSetState(0, acquires)
尝试替换同步状态来获取锁。
NonfairSync的不公平体现在获取锁前不会不会确认是否有线程正在等待锁,而是直接获取锁,如果获取失败,依旧会执行AQS的acquireQueued加入等待队列。
《AQS的今生,构建出JUC的基础》中,提到过ReentrantLock的重入性依赖于同步状态state作为计数器的特性实现,在公平锁FairSync和非公平锁NonfairSync的实现中我们也看到,线程重入时会执行同步状态+1的操作:
int nextc = c + acquires;
setState(nextc);
既然lock操作中有同步状态+1的操作,那么unlock操作中就一定有同步状态-1的操作:
public class ReentrantLock implements Lock, java.io.Serializable {
public void unlock() {
sync.release(1);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
// 线程退出,同步状态-1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean free = false;
if (c == 0) {
// 同步状态为0,锁未被持有,释放独占锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
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的实现并不复杂,同步状态-1后,如果同步状态为0,表示锁未被持有,修改锁的独占线程,然后更新同步状态。
我们再来看ReentrantLock的可重入性的实现,是不是非常简单了?判断是否是线程重入依赖的是getExclusiveOwnerThread方法,获取当前独占锁的线程,记录重入次数依赖的是同步状态作为计数器的特性。
现在能够理解为什么ReentrantLock中lock要与unlock操作成对出现了吧?最后,提个小问题,为什么lock和unlock操作中,只有当c == 0
时的lock操作需要使用CAS?
我们前面已经了解过ReentrantLock提供的4个加锁方法了,分别是:
public void lock()
,最常用的加锁方法,允许中断,但不会抛出异常,加锁失败进入等待队列;public void lockInterruptibly()
,允许中断,抛出InterruptedException异常,加锁失败进入队列直到被唤醒或者被中断;public boolean tryLock()
,尝试直接加锁,加锁失败不会进入队列,而是直接返回false;public boolean tryLock(long timeout, TimeUnit unit)
,尝试直接加锁,中断时抛出InterruptedException异常,加锁失败进入队列,直到指定时间内加锁成功,或者超时。lock方法的调用:
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
}
lockInterruptibly方法的调用:
public class ReentrantLock implements Lock, java.io.Serializable {
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
if (!tryAcquire(arg)) {
doAcquireInterruptibly(arg);
}
}
}
可以看到,差异主要体现在acquireQueued和doAcquireInterruptibly的实现上:
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return interrupted;
}
// 当parkAndCheckInterrupt为true时,修改interrupted标记为中断
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return;
}
// 当parkAndCheckInterrupt为true时,抛出异常
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
从源码上来看,差异体现在对parkAndCheckInterrupt结果的处理方式上,acquireQueued只标记中断状态,而doAcquireInterruptibly直接抛出异常。
public boolean tryLock()
的实现非常简单:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
直接调用Sync#nonfairTryAcquire
,在前面非公平锁的内容中我们已经知道nonfairTryAcquire只是进行了一次非公平的加锁尝试,如果没有调用AQS的acquireQueued不会加入到等待队列中。
tryLock的重载方法也并不复杂,按照之前的习惯,应该是有着特殊的acquireQueued实现:
public class ReentrantLock implements Lock, java.io.Serializable {
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
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);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
return true;
}
// 判断超时时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
// 调用LockSupport.parkNanos暂停指定时间
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanosTimeout);
// 线程中断抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
}
public boolean tryLock(long timeout, TimeUnit unit)
的特性依赖于LockSupport#parkNanos
暂停线程指定时间的能力。另外,我们可以注意到在判断是否需要park时,对nanosTimeout与SPIN_FOR_TIMEOUT_THRESHOLD的判断:
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD
时,认为一次park和upark对性能的影响小于自旋nanosTimeout纳秒;nanosTimeout < SPIN_FOR_TIMEOUT_THRESHOLD
时,认为一次park和upark对性能的影响大于自旋nanosTimeout纳秒。到这里我们就把4个加锁方法的差异讲完了,大体逻辑是相似的(如,唤醒头节点),只是为了实现某些特性添加了一些细节,大家可以认真阅读源码,很容易就能看出差异。
关于ReentrantLock的内容到这里就结束了,因为已经把AQS的部分单独拆了出来,所以今天并没有太复杂的内容。大家的重点可以放在ReentrantLock是如何借助AQS实现公平/非公平模式,以及可重入的特性上,诸如getHoldCount,isFair这类方法,相信大家已经能够想象到是如何实现的了,可以结合源码验证自己的想法。
最后,希望今天的内容能够帮助你更清晰的理解ReentrantLock,如果文章中出现错误,也希望大家不吝赐教。