在上两篇文章中,
聊聊并发:(七)concurrent包之AbstractQueuedSynchronizer分析
聊聊并发:(八)concurrent包之AbstractQueuedSynchronizer源码实现分析
我们介绍了AbstractQueuedSynchronizer同步器的工作原理与源码实现,了解AQS的实现机制对于我们理解Lock的各种锁的实现是至关重要的,本篇,我们来学习一下Lock的其中一个实现ReentrantLock可重入锁的实现机制。
ReentrantLock重入锁,是实现Lock接口的一个实现类,是我们开发并发程序中比较常用的一种锁,它支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。
我们在前面介绍synchronized关键字的时候提到过,synchronized的锁也是支持可重入的,只不过synchronized的可重入性是基于Java语言的实现机制实现,ReentrantLock是基于代码层面进行的实现。
相比于synchronized,ReentrantLock还支持公平锁和非公平锁两种方式。构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁与非公平锁的区别在于公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
接下来,我们具体来看一下它是如何实现的。
ReentrantLock如何获取锁:
//默认非公平锁
Lock nonFairLock = new ReentrantLock();
nonFairLock.lock();
//创建公平锁
Lock fairLock = new ReentrantLock(true);
fairLock.lock();
ReentrantLock获取锁的操作比较简单,直接调用其lock方法,即可完成锁的获取。ReentrantLock默认使用的是非公平锁,可以在构造方法中进行指定创建“公平锁”或“非公平锁”。
ReentrantLock的公平锁与非公平锁的实现是基于AQS进行实现的,其通过构造方法传入的值,构建不同的同步器,我们来看一下源码实现:
public void lock() {
sync.lock();
}
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock里面大部分的功能都是委托给Sync来实现的,同时Sync内部定义了lock()抽象方法由其子类去实现,默认实现了nonfairTryAcquire(int acquires)方法,可以看出它是非公平锁的默认实现方式。
非公平锁实现的实现是基于非公平同步器的,我们首先看一下非公平同步器的实现:
/**
* 非公平锁同步器
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//当前同步状态可用,直接将当前线程设置为获取到锁的线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//反之,调用AQS的acquire获取同步状态
acquire(1);
}
//重写AQS的tryAcquire方法的实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
首先第一次会检查当前同步状态是否可用,如果是,直接将当前线程设置为获取到锁的线程;
如果不是,则调用AQS中的acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法中首先会调用tryAcquire(),在介绍AQS的时候我们提到过,这个方法是由子类进行实现的,非公平同步器重写了tryAcquire方法,即会调用其子类的实现:
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前同步状态
int c = getState();
//如果同步状态可用
if (c == 0) {
//获取同步状态成功,CAS设置当前同步状态
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");
//更新同步状态
setState(nextc);
return true;
}
return false;
}
上面的代码就是非公平同步器获取同步状态的实现,我们在前面提到了,ReentrantLock是支持重入的锁,那么要想支持可重入,就需要就要解决两个问题:
- 1、线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
- 2、由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
我们再来看上面的代码,为了支持可重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。
OK,分析完非公平锁,我们接下来再看一下公平锁的实现,同理,公平锁是基于公平同步器的,我们看一下公平同步器的实现:
/**
* 公平锁同步器
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
//直接调用AQS的acquire获取同步状态
acquire(1);
}
//重写AQS的tryAcquire方法的实现
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前同步状态
int c = getState();
//如果同步状态可用
if (c == 0) {
//判断当前节点在同步队列中是否有前驱节点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//获取同步状态成功,CAS设置当前同步状态,并将同步状态置为当前线程所有
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方法,我们看一下该方法的实现:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//头节点 != 尾节点
//同步队列第一个节点不为null
//当前线程是同步队列第一个节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
该方法会判断判断当前节点在同步队列中是否有前驱节点,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。
公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。
ReentrantLock如何释放锁:
Lock lock = new ReentrantLock();
lock.lock();
//do something....
lock.unlock();
释放锁的操作很简单,使用完毕后,直接调用unlock方法,即可释放,建议释放锁的操作要在finally中进行,以免出现异常情况导致死锁:
lock.lock();
try {
//do something.....
} finally {
lock.unlock();
}
ReentrantLock释放锁的操作同样是基于同步器进行实现的:
public void unlock() {
sync.release(1);
}
同步器会调用AQS的release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release方法中,会调用tryRelease模板方法,同样,会调用其子类的实现(同样的套路有木有O(∩_∩)O哈哈~),我们来看一下子类的实现:
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);
}
setState(c);
return free;
}
上面的代码就是ReentrantLock的同步器中释放同步状态的逻辑,需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。
这里与上面的可重入部分的逻辑相对应,重入了多少次,在释放的时候,也要对应释放多少次,才算真正的释放成功。
现在我们已经了解了公平锁与非公平锁的实现机制,我们来对比一下两种锁的特性:
所以在选择使用哪种锁的时候,需要根据具体的业务场景进行判断:
- 如果对于顺序性要求比较严苛的场景,那么公平锁可能更加的合适;
- 如果对于顺序性不敏感的,但是对于并发吞吐量要求比较高的场景,那么使用非公平锁更加的合适。
ReentrantLock与synchronized都可以进行锁的实现,也都支持可重入性,那么它们之间又有什么相异之处呢?
首先他们肯定具有相同的功能和内存语义。
本篇我们介绍了ReentrantLock的使用方法以及实现机制,了解了其“公平锁”与“非公平锁的”实现机制。
ReentrantLock还有除了获取锁与释放锁的方法之外,还提供了很多其他的方法,例如设定超时获取锁、配合condition使用的方法等等,这里暂时不一一介绍,超时设定获取锁的方法可以参见之前介绍AQS的实现,condition的使用我们会在后面的篇幅中进行介绍。
本文参考:
JDK1.8源代码
JDK1.8中文文档
彻底理解ReentrantLock
更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java