在文章《java8 多线程并发之AQS详解》里面介绍了AQS的实现原理,ReentrantLock便是基于AQS实现的可重入锁。本文将详细介绍ReentrantLock的实现原理。
一、ReentrantLock
ReentrantLock实现了接口Lock,先来看一下Lock的定义:
public interface Lock {
//申请锁,如果没有申请到,则阻塞当前线程
//屏蔽线程中断
void lock();
//与lock()方法作用类似,如果发生了线程中断,该线程会抛出InterruptedException异常
void lockInterruptibly() throws InterruptedException;
//尝试加锁,如果加锁成功,则返回true,失败返回false
//无论加锁成功还是失败,该方法都会立即返回
boolean tryLock();
//尝试加锁,如果加锁成功,立即返回true,
//如果失败则会阻塞当前线程time时间,如果在time期间内申请到了,则返回true,如果没有申请到,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//解锁
void unlock();
//返回与锁相关的Condition对象
Condition newCondition();
}
ReentrantLock还有一个特性是公平锁和非公平锁。默认情况下,ReentrantLock是非公平锁,可以将构造方法的入参设置为true,这样ReentrantLock就变成了公平锁。
public ReentrantLock(boolean fair){}
公平锁与非公平锁的区别:
使用公平锁时,线程获取锁的顺序是按照线程申请锁的顺序来分配的,即先来先得,而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,它允许插队:当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。 非公平的ReentrantLock并不提倡插队行为,但是无法防止某个线程在合适的时候进行插队。
在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
相对来说,非公平锁的性能要高于公平锁。
非公平锁性能高于公平锁性能的原因:
线程进入RUNNABLE状态后,可以开始执行,但是到实际线程执行是要比较久的时间。而且,在一个锁释放之后,其他的线程会需要重新来获取锁。其中经历了持有锁的线程释放锁,其他线程从挂起恢复到RUNNABLE状态,其他线程请求锁,获得锁,线程执行,这一系列步骤。如果这个时候,存在一个线程直接请求锁,可能就避开挂起到恢复RUNNABLE状态的这段消耗,所以性能更优。
假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。
当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升可能不会出现。
二、源码分析
首先看一下ReentrantLock的构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从构造方法中可以看到,公平锁与非公平锁是由FairSync和NonfairSync两个类来实现的。而且ReentrantLock中的大部分方法都是直接调用FairSync和NonfairSync类中方法,比如lock()方法:
public void lock() {
sync.lock();
}
因此下面主要分析类FairSync和NonfairSync。
在介绍FairSync和NonfairSync之前,先介绍它们的基类Sync 。
1、Sync
Sync是ReentrantLock的内部抽象类,它继承自AQS(AbstractQueuedSynchronizer )。在介绍Sync源码之前,先介绍一下AQS中的属性state。在ReentrantLock中该属性表示是否有线程占用了锁。属性state的定义如下:
private volatile int state;
这个属性非常重要。如果state=0表示当前没有线程占用锁,其他线程可以申请锁,如果state>0,表示有线程已经占用了锁,一个线程可以多次占用锁,每占用一次,state就加1,state的值表示占用锁的个数;释放锁时,每释放一次,state响应的减1,等state=0了表示锁释放完毕。因为该属性的重要性,所以在代码中使用CAS更新该属性。可以调用AQS中的getState()方法获得该属性值。
下面看一下Sync的源码:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
//申请加锁,子类必须实现
abstract void lock();
//申请非公平锁,入参acquires表示申请锁的个数,一般是1
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//getState()是基类中的方法,getState()可以返回基类中state属性的值,
//如果返回0,表示当前没有线程持有锁,
//如果返回大于0,表示锁已经被线程占用了
int c = getState();
if (c == 0) {
//使用CAS更新基类的属性state,如果state不是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");
//使用CAS更新基类中的属性state
setState(nextc);
return true;
}
return false;
}
//释放锁,入参releases一般是1,由AQS中的release()方法调用
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//当前释放锁的线程与持有锁的线程不是同一个,则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c=0表示锁释放完毕
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//检查是否当前线程持有锁
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
//可以获得当前持有锁的线程对象
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
//获取当前线程持有锁的个数
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
//当前线程是否持有锁
final boolean isLocked() {
return getState() != 0;
}
}
2、FairSync
FairSync表示公平锁,继承自Sync,祖先类是AQS。
//代码有删减
static final class FairSync extends Sync {
//申请锁,里面调用了AQS中的acquire()方法
//acquire()里面首先调用本类中的tryAcquire(),如果没有加锁成功,
//则将当前线程放入队列中,等待下次申请锁
final void lock() {
acquire(1);
}
//公平锁与非公平锁之间的区别就体现在tryAcquire()方法上,每次加锁都要调用该方法
//该方法首先检查锁是否被占用了,如果是当前线程占用的,这次是再次加锁,那么允许加锁,该方法返回true;
//如果没有线程占用锁,那么检查队列中是否有线程等待锁,如果有则返回false,没有则加锁。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//调用父类方法获得属性state的值
int c = getState();
//c=0表示当前没有线程占用锁
if (c == 0) {
//hasQueuedPredecessors()方法用于检查队列中是否有线程等待锁
if (!hasQueuedPredecessors() &&
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;
}
}
当ReentrantLock是公平锁时,每次调用ReentrantLock.lock()申请锁时最终都会通过AQS调用到tryAcquire()方法尝试加锁,在tryAcquire()方法里面,首先检查是否有线程已经加锁了:
- 如果没有,则检查队列中是否有等待申请锁的线程(AQS将申请锁失败的线程放到一个FIFO队列中),如果有这样的线程,则tryAcquire()返回false,如果没有,则尝试加锁;
- 如果有线程已经加过锁了,那么检查持有锁的线程是否是当前线程,如果是则允许重入,修改state值。
在AQS里面,根据tryAcquire()方法的返回值有不同的处理,如果返回true,表示加锁成功,AQS里面不会再有其他处理,如果返回false,那么将当前线程放入队列尾,并阻塞当前线程。只有当队列中前面所有的线程都执行结束了,当前线程才会被唤醒并申请锁。
从tryAcquire()方法可以知道,公平锁是严格按照先进先出的原则申请锁的。
3、NonfairSync
NonfairSync表示非公平锁,也是继承自Sync。
static final class NonfairSync extends Sync {
final void lock() {
//与FairSync不同,直接先申请锁,如果申请失败了,才会调用AQS的acquire()
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//调用父类的方法加锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
看完FairSync,NonfairSync就很好理解了。与FairSync不同,申请锁之前,不会调用hasQueuedPredecessors()判断队列中是否有其他线程等待锁,而是直接尝试加锁,只有加锁失败了,才会将当前线程放入队列中。之后的处理过程,与FairSync类似。
注意:
ReentrantLock里面不带参数的tryLock()方法虽说也是加锁方法,但是无论是公平锁还是非公平锁,使用tryLock()加锁时,都不会检查队列是否是空,tryLock()里面直接调用了Sync类的nonfairTryAcquire()方法加锁:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
参考文章
https://blog.csdn.net/zhang199416/article/details/70792587