ReentrantLock,即重入锁,是一个和synchronized关键字等价的,支持线程重入的互斥锁。只是在synchronized已有功能基础上添加了一些扩展功能。
除了支持可中断获取锁、超时获取锁、非阻塞获取锁这些显示锁的常见功能外,ReentrantLock还支持公平锁(synchronized只支持非公平锁)。
一、使用
ReentrantLock takeLock = new ReentrantLock();
// 获取锁
takeLock.lock();
try {
// 业务逻辑
} finally {
// 释放锁
takeLock.unlock();
}
二、总体结构
重入锁的大体结构如下:
public class ReentrantLock implements Lock, java.io.Serializable {
private final ReentrantLock.Sync sync;
public ReentrantLock() {
this.sync = new ReentrantLock.NonfairSync();
}
public ReentrantLock(boolean var1) {
this.sync = (ReentrantLock.Sync)(var1?new ReentrantLock.FairSync():new ReentrantLock.NonfairSync());
}
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
//[Lock接口]获取锁. 成功则向下运行,失败则阻塞
public void lock() {
this.sync.lock();
}
//[Lock接口]可中断地获取锁,在当前线程获取锁的过程中可以响应中断信号
public void lockInterruptibly() throws InterruptedException {
this.sync.acquireInterruptibly(1);
}
//[Lock接口]尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false
public boolean tryLock() {
return this.sync.nonfairTryAcquire(1);
}
//[Lock接口]在超时时间内获取锁,到达超时时间将返回false,也可以响应中断
public boolean tryLock(long var1, TimeUnit var3) throws InterruptedException {
return this.sync.tryAcquireNanos(1, var3.toNanos(var1));
}
//[Lock接口]释放锁
public void unlock() {
this.sync.release(1);
}
//[Lock接口]获取等待通知组件实现信号控制,等待通知组件实现类似于Object.wait()方法的功能
public Condition newCondition() {
return this.sync.newCondition();
}
}
通过代码我们可以看到,基本全部的业务都是交给 Sync去实现的
- Sync类是AQS的子类,而NonfairSync和FairSync是Sync的子类。
- AQS里的state在重入锁里代表线程重入的次数,state=1代表重入锁当前已被某个线程独占,这个线程每重入一次,state++。因为state是int型变量,因此重入锁可以重入的最大次数是2^31-1。
- 公平锁和非公平锁的区别就在于获取锁时候的逻辑略有不同,其他操作都是一样的,因此公用的操作都放在Sync类里,NonfairSync和FairSync里只是实现自己的tryAcquire(int acquires)方法。
方法名称 | 描述 |
---|---|
void lock() | 获取锁. 成功则向下运行,失败则阻塞 |
void lockInterruptibly() throws InterruptedException | 可中断地获取锁,在当前线程获取锁的过程中可以响应中断信号 |
boolean tryLock() | 尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断 |
void unlock(); | 释放锁 |
Condition newCondition(); | 获取等待通知组件实现信号控制,等待通知组件实现类似于Object.wait()方法的功能 |
三、lock()与unlock()大致流程
lock()获取锁的主要流程如下:
- 首先,ReentrantLock的lock()方法会调用其内部成员变量sync的lock()方法;
- 其次,sync的非公平锁NonfairSync或公平锁FairSync实现了父类AbstractQueuedSynchronizer的lock()方法,其会调用acquire()方法;
- 然后,acquire()方法则在sync父类AbstractQueuedSynchronizer中实现
【ReentrantLock获取锁主体流程】
3.1 AQS的实现
在上篇我们已经讲到了,AQS需要重写的钩子方法:
方法名称 | 描述 |
---|---|
boolean tryAcquire(int arg) | 独占式尝试获取同步状态(通过CAS操作设置同步状态),如果成功返回true,反之返回false |
boolean tryRelease(int arg) | 独占式释放同步状态,成功返回true,失败返回false。 |
int tryAcquireShared(int arg) | 共享式的获取同步状态,返回大于等于0的值,表示获取成功,反之失败。 |
boolean tryReleaseShared(int arg) | 共享式释放同步状态,成功返回true,失败返回false。 |
boolean isHeldExclusively() | 判断同步器是否在独占模式下被占用,一般用来表示同步器是否被当前线程占用 |
我们来看下Sync对其的实现,可以看出Sync
中:
- 重写了
tryRelease
独占式释放同步状态 - 并没有重写
tryAcquire(int arg)
独占式尝试获取同步状态方法, 而是交给子类去实现 - 新建了
nonfairTryAcquire()
,这其实是个同步状态获取方法,在”非公平模式“的子类中被调用 - 定义了一个抽象的
lock()
方法,其实是暴漏给外部调用的独占式尝试获取同步状态方法。 在公平锁中,调用tryAcquire()
;在非公平所中调用nonfairTryAcquire()
- 一系列的Utils方法
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
Sync() {
}
/**************1. 重写了`tryRelease`独占式释放同步状态*********/
protected final boolean tryRelease(int var1) {
int var2 = this.getState() - var1;
if(Thread.currentThread() != this.getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
} else {
boolean var3 = false;
if(var2 == 0) {//作为可重入数,state在大于0时标识重入次数,必须退出对应次数时,才算真正的退出
var3 = true;
this.setExclusiveOwnerThread((Thread)null);
}
this.setState(var2);//更新锁状态
return var3;
}
}
//判断同步器是否在独占模式下被占用,一般用来表示**同步器是否被当前线程占用
protected final boolean isHeldExclusively() {
return this.getExclusiveOwnerThread() == Thread.currentThread();
}
/******2. 并没有重写`tryAcquire(int arg)`独占式尝试获取同步状态方法, 而是交给子类去实现***********/
//TODO
/******3. 新建了`nonfairTryAcquire()`,这其实是个同步状态获取方法,在”非公平模式“的子类中被调用*****/
final boolean nonfairTryAcquire(int var1) {
//下文讲解
}
/*********4. 定义了一个抽象的 `lock()`方法,其实是**暴漏给外部调用的独占式尝试获取同步状态方法**。*******/
abstract void lock();
/**************************5. 一系列的Utils方法******************************/
//当前锁是否已被占用
final boolean isLocked() {
return this.getState() != 0;
}
final Thread getOwner() {
return this.getState() == 0?null:this.getExclusiveOwnerThread();
}
final int getHoldCount() {
return this.isHeldExclusively()?this.getState():0;
}
final ConditionObject newCondition() {
return new ConditionObject(this);
}
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
this.setState(0);
}
}
}
3.1.1 非公平锁
通过代码可以看到,非公平锁上来就无视等待队列的存在而抢占锁
- 通过基于CAS操作的compareAndSetState(0, 1)方法,试图修改当前锁的状态
- 这个0表示AbstractQueuedSynchronizer内部的一种状态,针对互斥锁则是尚未有线程持有该锁,而>=1则表示存在线程持有该锁,并重入对应次数
- 这个上来就CAS的操作也是非公共锁的一种体现,CAS操作成功的话,则将当前线程设置为该锁的唯一拥有者。
- 抢占不成功的话,则调用父类的acquire()方法
- 我们已经知道这个
acquire(1)
AQS暴漏给外部使用的模板方法,内部其实会调用tryAcquire(int var1)
钩子方法 acquire(1)
独占式获取同步状态(该方法会调用子类重写的tryAcquire(int arg))。 如果当前线程获取同步状态成功,则返回以便继续执行,否则进入同步队列的尾部等待,该方法会调用tryAcquire(int arg)方法。
- 我们已经知道这个
static final class NonfairSync extends ReentrantLock.Sync {
private static final long serialVersionUID = 7316153563782823691L;
NonfairSync() {
}
//外部调用ReentrantLock.lock()其实会调用到这里
final void lock() {
if(this.compareAndSetState(0, 1)) {
this.setExclusiveOwnerThread(Thread.currentThread());
} else {
this.acquire(1);
}
}
protected final boolean tryAcquire(int var1) {
return this.nonfairTryAcquire(var1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//如果当前没有被占用
if (compareAndSetState(0, acquires)) {//则通过CAS来抢占,抢占成功,直接返回true
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//如果已经被占用,且就是当前线程占用。则将state+1,标识再次重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
3.1.2 公平锁
我们来看下公平锁:
- 调用父类的acquire()方法
- 我们已经知道这个
acquire(1)
AQS暴漏给外部使用的模板方法,内部其实会调用tryAcquire(int var1)
钩子方法 acquire(1)
独占式获取同步状态(该方法会调用子类重写的tryAcquire(int arg))。 如果当前线程获取同步状态成功,则返回以便继续执行,否则进入同步队列的尾部等待,该方法会调用tryAcquire(int arg)方法。
- 我们已经知道这个
前线程会在得到当前锁状态为0,即没有线程持有该锁,并且通过!hasQueuedPredecessors()判断当前等待队列没有前继线程(也就是说,没有比我优先级更高的线程在请求锁了)获取锁的情况下,通过CAS抢占锁,并设置自己为锁的当前拥有者,当然,如果是重入的话,和非公平锁处理一样,通过累加状态位标记重入次数。
而一旦等待队列中有等待者,或当前线程抢占锁失败,则它会乖乖的进入等待队列排队等待。 这部分由AQS的cquire()方法实现
static final class FairSync extends ReentrantLock.Sync {
private static final long serialVersionUID = -3000897897090466540L;
FairSync() {
}
final void lock() {
this.acquire(1);
}
protected final boolean tryAcquire(int var1) {
Thread var2 = Thread.currentThread();
int var3 = this.getState();
if(var3 == 0) {//如果当前没有被占用
if(!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {//先判断队列中是否有前置等待节点,如果没有。则通过CAS来抢占,抢占成功,直接返回true
this.setExclusiveOwnerThread(var2);
return true;
}
} else if(var2 == this.getExclusiveOwnerThread()) {//如果已经被占用,且就是当前线程占用。则将state+1,标识再次重入
int var4 = var3 + var1;
if(var4 < 0) {
throw new Error("Maximum lock count exceeded");
}
this.setState(var4);
return true;
}
return false;
}
}
我们可以发现,这段代码与上面的 nonfairTryAcquire方法就只多了一句代码,而就是这一句代码就实现了公平锁。
hasQueuedPredecessors()方法判断同步队列中是否有更早开始等待锁的线程。如果有,则tryAcquire方法直接返回false让当前线程进入同步队列排队。
四、tryLock()的实现
即便是公平锁,如果通过不带超时时间限制的tryLock()的方式获取锁的话,它也是不公平的,因为其内部调用的是sync.nonfairTryAcquire()方法,无论抢到与否,都会同步返回。如下:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
但是带有超时时间限制的tryLock(long timeout, TimeUnit unit)方法则不一样,还是会遵循公平或非公平的原则的,如下:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
为什么这么实现呢?
我们可以想一下tryLock() 的语义,tryLock() 要实现的效果是尝试获取一次锁,如果获取失败不阻塞而是直接返回false。
如果在公平锁模式下严格按照公平锁的定义来实现这个方法,那么当同步队列中有其他线程等待的时候,tryLock()都不可能获取到锁,只能返回false。而事实上,当我们调用tryLock()的时候,很多时候应该都是希望尽可能的成功的,而此时要不要让tryLock()的线程严格排队,其实不是那么重要,因此公平锁下tryLock()方法在获取锁时使用非公平获取模式,即可以插队。
那么如果我们在公平锁模式下就希望tryLock()方法获取锁严格排队呢?可以用tryLock(0, TimeUnit.SECONDS),这个方法等效于一个严格排队的tryLock()方法,之所以等效,是因为tryLock(long timeout, TimeUnit unit)的实现是区分公平锁和非公平锁的,在公平锁的模式下,获取锁的操作是严格按同步队列排队等待的。
那么如果我们在公平锁模式下希望 tryLock(long timeout, TimeUnit unit) 不严格排队,表现的像一个支持超时的tryLock()呢?也是有办法的:
if (lock.tryLock() || lock.tryLock(timeout, unit)) {
...
}
可以这样组合一下,左边的tryLock()有机会插队获取一次锁,如果没获取到,在用tryLock(timeout, unit)做一次可超时的同步队列排队。