什么是ReentrantLock
ReentrantLoock也称为可重入锁。可重入意味着已持有锁的线程,可以重复地获取锁,表现为,如果锁不支持重入,那么当持有锁的线程再次上锁时将会被阻塞住。如当在递归的方法里获取锁时,递归将因申请锁而被阻塞住。
锁的知识分两部分,一分部为如何加解锁,另一部分为把锁分配给谁。ReentrantLoock依赖于AQS,AQS解决了把锁分配给谁的问题,ReentrantLoock只需解决如何加解锁即可。
AQS原理可以参考一文了解AQS,了解AQS将使ReentrantLock变得简单,不了解也不妨碍理解。
ReentrantLock的特点为:
- 可重入
- 为独占锁,同一时间只有一个线程能获取到锁
- 支持公平与非公平的方式获取锁,公平意味着,按申请顺序锁的顺序分配锁
- 继承了AQS的其他特性
ReentrantLock实现了Lock语义,Lock接口代表着锁所应具有的方法模板
public interface Lock {
void lock(); // 获取锁,获取不到会被阻塞
void lockInterruptibly() throws InterruptedException; // 获取锁,可被中断,获取不到会被阻塞
boolean tryLock(); // 获取锁,无论结果如何不会被阻塞
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 获取锁,最多在unit时间内返回结果,可被中断
void unlock(); // 释放锁
Condition newCondition(); // 支持满足一定条件后,再去上锁
}
AQS基础
假设我们已经了解了加解锁的控制,现在先简要地了解AQS是如何分配锁的:
- 当申请锁,即调用了与acquire()类似语义的方法时,AQS将询问子类是否上锁成功,成功则继续运行。否则,AQS将以Node为粒度,记录这个申请锁的请求,将其插入自身维护的CLH队里中并挂起这个线程
- 在CLH队列中,只有最靠近头节点的未取消申请锁的节点,才有资格申请锁
- 当线程被唤醒时,会尝试获取锁,如果获取不到继续挂起;获取得到则继续运行
- 当一个线程释放锁,即调用release()类似语义的方法时,AQS将询问子类是否解锁成功,有锁可以分配,如果有,AQS从CLH队列中主动唤起合适的线程,过程为2、3
- 如果需要等待条件满足再去申请锁,即调用了wait()类似语义的方法时,在AQS中表现为,以Node为粒度,维护一个单向等待条件队列,把Node所代表的线程挂起
- 当条件满足时,即调用了signal()类似语义的方法时,唤醒等待条件队列最前面的未取消等待的Node,执行1
- 子类可以维护AQS的state属性来记录加解锁状态,AQS也提供了CAS的方法compareAndSetState()抢占更新state
简要来说,AQS分配锁时,当前线程可能会被挂起,接着被唤醒继续尝试申请锁,重复此过程直到获取到锁或取消等待。从外部看,就如入口方法被阻塞并在未来被恢复了一样。
Sync
现在,就可以来看看ReentrentLock是怎么回事。在ReentrentLock中,以内部类Sync继承AQS,之后要说到的公平锁和不公平锁也是基于Sync的子类实现。Sync主要的方法为 nonfairTryAcquire() 和 tryRelease()。
final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 被加锁的次数
int c = getState();
if (c == 0) {
// 说明没有线程获得锁
if (compareAndSetState(0, acquires)) {
// 加锁次数通过CAS设置为acquires次
// 记住拿到锁的线程
setExclusiveOwnerThread(current);
// 加锁成功
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 被加锁的次数不为0,说明被某个线程持有,进到这里说明重入,即同一线程再次加锁了
// 要更新的加锁次数
int nextc = c + acquires;
// 加锁次数无论如何都不会小于0,如果小于0,说明发生了并发错误
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新加锁次数
setState(nextc);
// 加锁成功
return true;
}
// 加锁失败
return false;
}
nonfairTryAcquire()以不公平的方式获取锁,当持有锁的线程再次申请锁时,可以再次上锁,发生了重入。值得说明的是,在此方法中通过setState()更新上锁次数是,不需要考虑并发性,因为这是发生在同一线程的连续行为。
protected final boolean tryRelease(int releases) {
// 释放锁后,要跟新的加锁次数
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
// 非申请锁的线程,不能释放锁
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 说明释放后,没有锁被线程持有了
// 如果没有进入到这里,说明还有锁被当前的线程持有,之前有重入
free = true;
// 没有拿到锁的线程了
setExclusiveOwnerThread(null);
}
// 更新了上锁的次数
setState(c);
// true代表告诉AQS有锁可以继续分配了,false表示告诉AQS没有锁可以分配
return free;
}
tryRelease()为AQC要求子类实现的,询问是否解锁成功并有锁可以继续进行分配的方法。与前面nonfairTryAcquire()相同,不需要考虑并发,只有加锁的线程才会调用到。
其他的方法简要说明为:
- lock(): 子类要实现的加锁入口方法
- isHeldExclusively():检查锁的持有线程是否是当前线程
- newCondition():返回AQS支持的,允许线程等待条件再申请锁的入口操作对象
- getOwner():返回当前持有锁的线程
- getHoldCount():被持有的锁数量
- isLocked():是否有线程持有锁
- readObject():从序列化中恢复Sync
ReentranLock默认为不公平锁,可以通过实例化方法参数,决定使用公平锁或不公平锁,其实现均为Sync的子类。
不公平锁的实现
不公平锁的实现就比较简单了,Sync已经支持nonfairTryAcquire()以不公平的方式支持获取锁,那么代表不公平锁的实现NonfairSync只需要维持AQS首次请求分配锁时不公平的特点即可(AQS基础一节图中首次请求锁没有要求入队等待,所以有可能插队)。
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
// ① 直接上锁成功,不用从AQS中等待了
setExclusiveOwnerThread(Thread.currentThread());
else
// 从AQS中请求所分配
acquire(1);
}
// 这个方法是AQS要求子类实现,并向子类寻求加锁的方法
protected final boolean tryAcquire(int acquires) {
// 直接调用了Sync的方法
return nonfairTryAcquire(acquires);
}
}
tryAcquire()为AQS要求子类实现的,并向其询问是否成功加锁的方法。①中,如果能上锁,就不麻烦AQS去分配锁了,上锁状态也通过CAS保存了。那么,这里是可以插队的。设想某一时刻,AQS中有在队列中等待分配锁的代表线程的Node,此时有线程释放了锁,然后时间片分配给了刚刚调用到lock()的线程,那么这个线程拿到了锁,插了队。同样的情况通过AQS的acquire()也有机会发生。
公平锁的实现
不公平锁由Sync的子类FairSync实现。
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 加锁的次数
int c = getState();
if (c == 0) {
// ① 没有线程持有锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 进到这里说明,1.队列中没有等待分配锁的线程 2.获取到了锁,因为成功通过CAS更新了加锁次数
setExclusiveOwnerThread(current);
// 告诉AQS获取到了锁
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 说明当前线程重入
// 要更新的上锁次数
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 更新锁次数
setState(nextc);
// 告诉AQS加锁成功
return true;
}
// 告诉AQS加锁失败
return false;
}
}
在①中,FairSync处理了插队的情况。当FairSync通过acquire()让AQS分配锁时,虽然AQS会先通过tryAcquire()让FairSync上锁,不成功再把申请锁的线程加入队列。但是FairSync需要保持公平地特性,那么通过检查hasQueuedPredecessors()发现有代表线程的Node在等待锁时,直接返回加锁不成功,杜绝了插队。
总结
Lock接口的其他方法语义,ReentranLock未作封装直接使用了AQS的特性就可实现,因此不做展开。对与ReentranLock的使用,按照Lock的接口语义使用即可,最简单的加锁解锁方法分别为Lock.lock()、Lock.unLock()。
那么,ReentranLock的意义为,拓展了AQS,实现了可重入的,公平或不公平的特性。
- 可重入的实现为:当AQS向它的子类Sync(的子类)申请加锁发生失败时,Sync将判断申请锁的线程是否为持有锁的线程,如果是则增加上锁的次数,允许通过,也就发生了重入。那么,重入产生的多余的上锁次数,也只有上锁的线程能释放。
- 公平或不公平的实现为:当向AQS申请分配锁时,AQS向它的子类Sync(的子类)申请加锁,如果失败,就将申请锁的请求放到CLH队列中排队。那么,Sync就可以通过发现CLH队里中还有线程排队时,直接告诉AQS加锁是否失败,决定了锁是否公平。于NonfairSync,它忽略了AQS中队列的情况直接尝试插队;于FairSync,它发现队列还有等待的代表申请锁的线程的Node时,不直接加锁。
综上,ReentranLock并没有过多的特别操作,也没有属于自己的生硬的技术点,也就是说理解ReentranLock的程度,取决于理解AQS的程度。不仅仅是ReentranLock,Java中的许多锁也都依赖于AQS。因此,仅学习ReentranLock还不够,还需要进一步拿下AQS,才能一窥Java众多并发的核心秘密。