ReentrantLock是Java并发包中提供的一个可重入的互斥锁。ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性。只不过相比原生的Synchronized,ReentrantLock增加了一些高级的扩展功能,比如它可以实现公平锁,同时也可以绑定多个Conditon。
可重入性/公平锁/非公平锁
可重入性
所谓的可重入性,就是可以支持一个线程对锁的重复获取,原生的synchronized就具有可重入性,一个用synchronized修饰的递归方法,当线程在执行期间,它是可以反复获取到锁的,而不会出现自己把自己锁死的情况。ReentrantLock也是如此,在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
公平锁/非公平锁
所谓公平锁,顾名思义,意指锁的获取策略相对公平,当多个线程在获取同一个锁时,必须按照锁的申请时间来依次获得锁,排排队,不能插队;非公平锁则不同,当锁被释放时,等待中的线程均有机会获得锁。synchronized是非公平锁,ReentrantLock默认也是非公平的,但是可以通过带boolean参数的构造方法指定使用公平锁,但非公平锁的性能一般要优于公平锁。
synchronized是Java原生的互斥同步锁,使用方便,对于synchronized修饰的方法或同步块,无需再显式释放锁。synchronized底层是通过monitorenter和monitorexit两个字节码指令来实现加锁解锁操作的。而ReentrantLock做为API层面的互斥锁,需要显式地去加锁解锁。
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // 加锁
try {
// ... 函数主题
} finally {
lock.unlock() //解锁
}
}
}
源码分析
接下来我们从源码角度来看看ReentrantLock的实现原理,它是如何保证可重入性,又是如何实现公平锁的。
Sync
ReentrantLock是基于AQS的,Sync为ReentrantLock里面的一个内部类,它继承AQS(AbstractQueuedSynchronizer。它有两个子类:公平锁FairSync和非公平锁NonfairSync。
ReentrantLock里面大部分的功能都是委托给Sync来实现的,同时Sync内部定义了lock()抽象方法由其子类去实现,默认实现了nonfairTryAcquire(int acquires)方法,可以看出它是非公平锁的默认实现方式。下面Sync类的实现:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
//加锁方法,由子类自己实现
abstract void lock();
//非公平尝试获取锁的方法
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取AQS中的状态
int c = getState();
//若state为0,意味着没有线程获取到资源
if (c == 0) {
//CAS将state设置为1
if (compareAndSetState(0, acquires)) {
//将当前线程标记我获取到排他锁的线程,返回true
setExclusiveOwnerThread(current);
return true;
}
}
//若state不为0,但是持有锁的线程是当前线程
else if (current == getExclusiveOwnerThread()) {
//state累加1
int nextc = c + acquires;
//int类型溢出了,抛出异常
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//设置state,此时state大于1,代表着一个线程多次获锁,state的值即是线程重入的次数
setState(nextc);
return true;
}
//获取锁失败
return false;
}
//尝试释放锁
protected final boolean tryRelease(int releases) {
//减掉releases
int c = getState() - releases;
//如果释放的不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//state == 0 表示已经释放完全了,其他线程可以获取同步状态了
if (c == 0) {
free = true;
//清空保存的锁持有的线程对象
setExclusiveOwnerThread(null);
}
//设置AQS状态
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;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0);
}
}
我们看到Sync类提供了非公平的尝试获取锁和释放锁的方法,还提供了一些查询锁状态和锁持有线程信息的方法。由于是非公平所以无需考虑线程调用的顺序。由于要支持重入,所以需要报错当前占用锁的线程,并在获取锁时进行判断。一个线程再占用锁之后重复获取锁状态会累加,同时也需要对应着多次释放的操作。
公平锁和非公平锁
公平锁与非公平锁的区别在于公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
NonfairSync
非公平
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//非公平加锁方式
final void lock() {
//CAS设置state状态,若原值是0,将其置为1
if (compareAndSetState(0, 1))
//将当前线程标记为已持有锁
setExclusiveOwnerThread(Thread.currentThread());
else
///若设置失败,调用AQS的acquire方法,acquire又会调用我们下面重写的tryAcquire方法。这里说的调用失败有两种情况:1当前没有线程获取到资源,state为0,但是将state由0设置为1的时候,其他线程抢占资源,将state修改了,导致了CAS失败;2 state原本就不为0,也就是已经有线程获取到资源了,有可能是别的线程获取到资源,也有可能是当前线程获取的,这时线程又重复去获取,所以去tryAcquire中的nonfairTryAcquire我们应该就能看到可重入的实现逻辑了。
acquire(1);
}
//调用父类尝试获取锁的方法
protected final boolean tryAcquire(int acquires) {
//调用Sync中的方法
return nonfairTryAcquire(acquires);
}
}
非公平锁的lock方法在发生抢占时会调用AQS提供的模板方法acquire获取许可,AQS中会调用AQS子类重新的tryAcquire尝试获取许可,tryAcquire是由父类实现的。
FairSync
公平
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
直接调用AQS的模板方法acquire,acquire会调用下面我们重写的这个tryAcquire
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state值
int c = getState();
//若state为0,意味着当前没有线程获取到资源,那就可以直接获取资源了吗?NO!这不就跟之前的非公平锁的逻辑一样了嘛。看下面的逻辑
if (c == 0) {
//判断在时间顺序上,是否有申请锁排在自己之前的线程,若没有,才能去获取,CAS设置state,并标记当前线程为持有排他锁的线程;反之,不能获取!这即是公平的处理方式。
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;
}
}
可以看到,公平锁的大致逻辑与非公平锁是一致的,不同的地方在于有了!hasQueuedPredecessors()这个判断逻辑,即便state为0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,做后面的处理。反之,返回false,获取失败。
看看这个判断是否有排队中线程的逻辑。
public final boolean hasQueuedPredecessors() {
// 尾结点
Node t = tail;
//头结点
Node h = head;
Node s;
//判断是否有排在自己之前的线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
需要注意的是,这个判断是否有排在自己之前的线程的逻辑稍微有些绕,我们来梳理下,由代码得知,有两种情况会返回true,我们将此逻辑分解一下(注意:返回true意味着有其他线程申请锁比自己早,需要放弃抢占):
-
h !=t && (s = h.next) == null, 这个逻辑成立的一种可能是head指向头结点,tail此时还为null。考虑这种情况:当其他某个线程去获取锁失败,需构造一个结点加入同步队列中(假设此时同步队列为空),在添加的时候,需要先创建一个无意义傀儡头结点(在AQS的enq方法中,这是个自旋CAS操作),有可能在将head指向此傀儡结点完毕之后,还未将tail指向此结点。很明显,此线程时间上优于当前线程,所以,返回true,表示有等待中的线程且比自己来的还早。
-
h != t && (s = h.next) != null && s.thread != Thread.currentThread()。 同步队列中已经有若干排队线程且当前线程不是队列的老二结点,此种情况会返回true。假如没有s.thread !=Thread.currentThread()这个判断的话,会怎么样呢?若当前线程已经在同步队列中是老二结点(头结点此时是个无意义的傀儡结点),此时持有锁的线程释放了资源,唤醒老二结点线程,老二结点线程重新tryAcquire(此逻辑在AQS中的acquireQueued方法中),又会调用到hasQueuedPredecessors,不加s.thread !=Thread.currentThread()这个判断的话,返回值就为true,导致tryAcquire失败。
其他方法
无参构造器(默认为非公平锁)
public ReentrantLock() {
//默认是非公平的
sync = new NonfairSync();
}
无参构造方法,默认是非公平的。
带布尔值的构造器(是否公平)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可以通过传入true和false,指定公平还是非公平的。
lock()
public void lock() {
sync.lock();
}
Sync的lock方法是抽象的,实际的lock会代理到FairSync或是NonFairSync上(根据用户的选择来决定,公平锁还是非公平锁)。
tryLock()
public boolean tryLock() {
return sync.nonfairTryAcquire(1);//代理到sync的相应方法上
}
tryLock,尝试获取锁,成功则直接返回true,不成功也不耽搁时间,立即返回false。
unlock()
public void unlock() {
sync.release(1);//释放锁
}
释放锁,调用sync的release方法,其实是AQS的release逻辑。
ReentrantLock与synchronized的区别
前面提到ReentrantLock提供了比synchronized更加灵活和强大的锁机制,那么它的灵活和强大之处在哪里呢?他们之间又有什么相异之处呢?
首先他们肯定具有相同的功能和内存语义。
- 与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
- ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合(以后会阐述Condition)。
- ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
- ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
- ReentrantLock支持中断处理,且性能较synchronized会好些。
总结
ReentrantLock是一种可重入的,可实现公平性的互斥锁,它的设计基于AQS框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state就加1,当然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一个判断:是否有比自己申请早的线程在同步队列中等待,若有,去等待;若没有,才允许去抢占。