16.ReentrantLock全解读
大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人。
经历了AQS的前世和今生后,我们已经知道AQS是Java中提供同步状态原子管理,线程阻塞/唤醒,以及线程排队功能的同步器基础框架。那么我们今天就来学习通过AQS实现的ReentrantLock
。按照惯例,先来看3道关于ReentrantLock
的常见面试题:
-
什么是
ReentrantLock
? -
ReentrantLock
内部原理是怎样的?如何实现可重入性? -
ReentrantLock
和synchronized
有什么区别?该如何选择?
接下来,我会尽可能的通过剖析源码的方式为大家解答以上的题目。
ReentrantLock是什么?
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
还实现了公平和非公平两种模式。
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
还提供了用于分析锁的方法:
方法声明 | 作用 |
---|---|
public int getHoldCount() |
返回当前线程持有锁的次数,即当前线程重入锁的次数 |
public final int getQueueLength() |
返回等待获取锁的线程数量估算值 |
public final boolean hasQueuedThread(Thread thread) |
查询当前线程是否在等待获取锁 |
public final boolean hasQueuedThreads() |
是否有线程在等待获取锁 |
public final boolean isFair() |
是否为公平锁 |
public boolean isHeldByCurrentThread() |
当前线程是否持有锁 |
public boolean isLocked() |
锁是否被线程持有,即锁是否被使用 |
public Condition newCondition() |
创建条件对象 |
public int getWaitQueueLength(Condition condition) |
等待在该条件上的线程数量 |
public boolean hasWaiters |
是否有线程在等待在该条件上 |
ReentrantLock源码分析
接下来,我们通过源码来分析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
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
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与lockInterruptibly
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
直接抛出异常。
tryLock与它的重载方法
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
,如果文章中出现错误,也希望大家不吝赐教。
好了,今天就到这里了,Bye~~