图解java.util.concurrent并发包源码系列——深入理解ReentrantLock,看完可以吊打面试官
- ReentrantLock是什么,有什么作用
- ReentrantLock的使用
- ReentrantLock源码解析
- ReentrantLock#lock方法
- FairSync#tryAcquire方法
- NonfairSync#tryAcquire方法
- ReentrantLock#unlock方法
- ReentrantLock的其他方法简介
- ReentrantLock#lockInterruptibly
- ReentrantLock#tryLock
- ReentrantLock#tryLock(带参数)
- ReentrantLock#newCondition
往期文章:
- 人人都能看懂的图解java.util.concurrent并发包源码系列 ThreadPoolExecutor线程池
- 图解java.util.concurrent并发包源码系列,原子类、CAS、AtomicLong、AtomicStampedReference一套带走
- 图解java.util.concurrent并发包源码系列——LongAdder
- 图解java.util.concurrent并发包源码系列——深入理解AQS,看完可以吊打面试官
上一篇文章(图解java.util.concurrent并发包源码系列——深入理解AQS,看完可以吊打面试官 )介绍了AQS的原理和源码,然后使用AQS实现了一个排他锁,这次我们看看Java的并发包里面基于AQS实现的锁工具类——ReentrantLock。本文会介绍ReentrantLock的功能,以及ReentrantLock如何通过AQS实现它的功能,但是不会再介绍AQS的相关原理,关于AQS的原理和源码,可以看上一篇文章。
ReentrantLock是什么,有什么作用
ReentrantLock是可重入锁,当我们有一些资源需要互斥访问,或者有某一块代码需要互斥执行时,可以使用ReentrantLock加锁,对互斥资源和互斥代码块起到一个保护作用,保证同一时刻只会有一个线程访问。
可重入的意思就是当一个线程获取到锁之后,如果它再次获取同一把锁,是可以获取成功的。而不可重入锁则是当前线程获取到锁以后,如果它再次获取,是获取不成功的,也就是自己把自己给阻塞住了,我们上一篇文章最后面的例子就是一个不可重入锁。
ReentrantLock的使用
ReentrantLock的用法非常简单,它提供了lock方法用于获取锁,unlock方法用于释放锁。
调用lock方法后,当前线程会去获取锁,获取不到锁会阻塞等待,直到获取到锁,该方法才会返回。如果重复调用多次lock方法,就重复加锁多次,当前线程不会阻塞(可重入),但是它需要多次释放锁。
调用unlock方法,当前线程就会释放锁,然后唤醒其他阻塞等待获取锁的线程。
我们还是用上一篇文章最后的那个例子,去观察ReentrantLock的作用。
public static void main(String[] args) throws InterruptedException {
int[] num = {0};
// 创建一个ReentrantLock对象
ReentrantLock reentrantLock = new ReentrantLock();
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
try {
// 加锁
reentrantLock.lock();
num[0]++;
} finally {
// 释放锁,防止finally块中是因为防止抛异常导致锁得不到释放
reentrantLock.unlock();
}
}
});
thread.start();
threads[i] = thread;
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println(num[0]);
}
可以看到输出结果是正确的。
用法跟我们上一篇文章最后面的例子几乎是一模一样的。首先要创建一个ReentrantLock对象,然在线程try代码块中调用ReentrantLock的lock方法进行加锁,在finally代码块中调用ReentrantLock的unlock方法进行解锁操作,解锁操作在finally块中是因为防止线程抛异常导致锁得不到释放。
ReentrantLock源码解析
接下来看看ReentrantLock的lock方法和unlock方法的内部源码,看看它是怎么实现锁的获取和释放的。
ReentrantLock#lock方法
ReentrantLock#lock
public void lock() {
sync.lock();
}
可以看到和我们上一篇文章的例子一样,都是有一个内部类,这个内部类肯定继承了AQS。但是它这里没有直接调用AQS的acquire方法,而是调用了一个lock方法,我们进去lock方法看一看。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
// ...省略了下面的代码
}
可以看到Sync继承了AQS,但是Sync本身还是一个抽象类,而lock方法也是一个抽象方法。
可以看到Sync有两个实现类,FairSync和NonfairSync。
也就是说ReentrantLock内部的sync有可能是FairSync,也有可能是NonfairSync。那什么时候是FairSync、什么时候是NonFairSync呢?我们看看ReentrantLock的构造方法就知道了。
/**
* 默认非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* fair为true时,是公平锁,为false则是非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock可以实现公平锁和非公平锁的功能,默认是非公平锁,如果我们调用有参构造方法,传递的fair参数是true就是公平锁,使用的sync就是FairSync,否则就是非公平锁,使用的就是NonfairSync。
公平锁就是当一个线程获取锁时,会先看一下队列中是否有线程等待获取锁,如果有,那么它会入队列。非公平锁则相反,如果一个线程要获取锁,那么不管三七二十一,先尝试获取一下,如果获取成功,则不用进队列。非公平锁的效率是比公平锁高的,所有默认的就是非公平锁。
我看一下lock方法的具体实现。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// ......省略下面的代码
}
FairSync的lock方法直接调用了AQS的acquire方法,与我们上一篇文章的例子一致。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// ......省略下面的代码
}
NonfairSync的lock方法,会使用AQS的compareAndSetState方法尝试获取锁,如果CAS成功那么就不需要走acquire方法的逻辑了,如果CAS失败,才会调用AQS的acquire方法。
FairSync#tryAcquire方法
FairSync的lock方法会调用AQS的acquire方法,acquire方法会调用tryAcquire方法,然后会进入到FairSync的tryAcquire方法。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取AQS中的state变量
if (c == 0) { // state等于0表示锁没有被获取
if (!hasQueuedPredecessors() && // 查看队列中是否有等待获取锁的线程
compareAndSetState(0, acquires)) { // CAS尝试获取锁
setExclusiveOwnerThread(current); // 获取锁成功,设置当前线程为占有锁的线程
return true; // 返回true,表示告诉AQS获取锁成功
}
}
else if (current == getExclusiveOwnerThread()) { // 锁已被获取,但是获取锁的线程是当前线程
int nextc = c + acquires; // state加acquires
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); // 设置state的值
return true; // 返回true,表示告诉AQS获取锁成功
}
return false; // 返回true,表示告诉AQS获取锁不成功
}
}
FairSync的tryAcquire方法首先会调用AQS的getState方法获取state变量,这里是当state为0才表示锁没有被获取,与我们上一篇文章的例子不一样。
如果state为0,那么锁没有被获取,此时会调用hasQueuedPredecessors()方法检查队列中是否有等待获取锁的线程,如果有,那么当前线程不会获取锁,而是返回false直接进队列排队,如果队列中没有等待获取锁的线程,那么会调用AQS的compareAndSetState方法尝试修改state变量,修改成功表示获取锁成功,此时就会设置当前线程为占有锁的线程,然后方法返回true,表示获取锁成功。
如果state不为0,那么会检查当前线程是否是占有锁的线程,如果是,那么会执行重入的逻辑,然后方法返回true。如果当前线程不是占有锁的线程,那么方法返回false,表示获取锁失败。
下面看一下hasQueuedPredecessors方法是怎么判断队列中是否有线程的。
public final boolean hasQueuedPredecessors() {
Node t = tail; // 尾节点
Node h = head; // 头节点
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
h != t 判断头节点和尾节点是否不相等,不相等才有可能有线程在队列中,相等的话表示队列为空,那么肯定是没有线程在队列中的。
如果头节点和尾节点不相等了,那么看h.next头节点的后继节点是否为空,如果为空,那么就表示当前队列已经有等待获取锁的线程。那么为什么头尾节点不相等,并且头节点的next指针为空,就表示有线程已经入队列了呢?还有,什么情况下头节点指针和尾节点指针不相等,但是头节点的next指针为null呢?答案就在AQS初始化队列以及线程节点入队的方法中,也就是AQS的enq方法。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) // A:使用CAS的方式设置头节点指针指向一个空节点
tail = head; // B:设置尾指针指向和头指针指向的同一节点
} else {
node.prev = t; // C:设置node的prev指针指向当前队列的尾节点
if (compareAndSetTail(t, node)) { // D:使用CAS的方式设置尾节点指针指向当前node
t.next = node; // E:设置原来的尾节点的next指针指向当前node
return t;
}
}
}
}
可以看到AQS的enq方法,分成了5个步骤。注意!这里的前提是AQS的队列还未初始化!
假设现在AQS队列还未初始化,在当前线程获取锁之前,有一个线程执行到了enq方法,然后这个线程执行完A、B、C、D四步,也就是它成功初始化了AQS的队列,然后又用CAS的方式修改尾节点的指针指向它自己的node节点,但是就是没有执行代码E,此时是不是头节点指针和尾节点指针不相等(h != t),并且头节点的next指针是null((s = h.next) == null)?此时这个线程已经算是入了队列的,如果此时有其他线程来获取锁(公平锁),那么是应该要排队的。所以当前线程获取锁的时候,执行到hasQueuedPredecessors()方法,就会返回true,表示已经有线程在队列中等待获取锁了。
那么假设现在AQS队列还未初始化,如果在当前线程获取锁之前,有一个线程执行到了enq方法,然这个线程执行完A、B,没有执行后面三步呢?那么此时头节点指针和尾节点指针还是相等的(h != t 不成立),此时这个线程也不算入队列,所以此时如果有别的线程来调用hasQueuedPredecessors()方法,hasQueuedPredecessors()方法放回false(队列中没有等待获取锁的线程),也是正确的。
如果头节点指针和尾节点指针不相等(h != t 为true),然后头节点的next指针不为null((s = h.next) == null 为false),但是头节点的next指针指向的节点对应的线程等于当前线程(s.thread != Thread.currentThread() 为false),那么hasQueuedPredecessors()方法返回false,当前线程可以尝试获取锁,也是正确的,因为很明显这种情况就是我已经在排队了,并且此时轮到我了。如果头节点的next指针指向的节点对应的线程不是当前线程(s.thread != Thread.currentThread() 为true),hasQueuedPredecessors()方法返回true,那么当前线程就只能去排队。
FairSync的tryAcquire方法就介绍到这里。
NonfairSync#tryAcquire方法
NonfairSync的tryAcquire方法里面直接调用了nonfairTryAcquire方法。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire方法位于Sync抽象类内部。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 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");
setState(nextc);
return true;
}
// 获取锁失败
return false;
}
进入到Sync的nonfairTryAcquire方法内部,如果判断锁没有被获取(state == 0)就直接先CAS一下,如果CAS成功,该方法就返回true,表示获取锁成功。如果锁已被获取,那么判断当前线程是否是持有锁的线程 (current == getExclusiveOwnerThread()),如果当前线程是持有锁的线程,那么执行锁重入的逻辑,然后方法返回true,表示获取锁成功。否则返回false,表示获取锁失败。
ReentrantLock的lock方法到这里就介绍完了。
ReentrantLock#unlock方法
接下来看一下ReentrantLock的unlock方法的具体逻辑。
public void unlock() {
sync.release(1);
}
ReentrantLock的unlock方法调用了sync的release方法,这个和我们上一篇文章的例子是一样的。
AQS的release方法会调用tryRelease方法。因为释放锁的操作不需要区分公平还是非公平,都是一样的释放锁的逻辑,所以tryRelease方法就放到了Sync抽象类的内部。也就是说,FairSync和NonfairSync的tryRelease是同一个方法,都是Sync的tryRelease方法。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是持有锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// c == 0,表示锁被释放,free设为true表示锁被释放,设置持有锁的线程为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新state
setState(c);
return free;
}
Sync的tryRelease方法首先会调用AQS的getState方法获取state变量,然后计算state变量减去参数releases之后的值c。然后判断如果c等于0,表示锁被释放了,那么设置返回值free为true表示锁被释放,然后setExclusiveOwnerThread(null)设置持有锁的线程为null。然后不管c是否等于0,最后都会调用AQS的setState方法,更新state变量,最后返回free。
如果返回的free为true,表示锁被释放了,如果返回的free为false,表示锁还没有被释放。比如当前线程重复获取锁5次,现在调用了unlock方法3次,那么state等于2,锁还没有被释放,它需要再调用两次,state才等于0,此时锁才被真正释放。
到这里ReentrantLock的unlock方法也介绍完了。
ReentrantLock的其他方法简介
ReentrantLock不仅仅只有lock和unlock两个方法,还有其他获取和释放锁的方法,下面做一个简单介绍,不做深入的描述。
ReentrantLock#lockInterruptibly
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
ReentrantLock的lockInterruptibly是以响应中断的方式获取锁,就是说在获取锁的过程中如果其他线程调用了thread.interrupt()方法打断了当前线程,那么当前线程会响应中断,不再继续获取锁。而我们上面介绍的ReentrantLock#lock方法,是不响应中断的,也就是说其他线程调用了当前线程的thread.interrupt()方法,当前线程也不做任何响应。
ReentrantLock#tryLock
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
ReentrantLock的tryLock方法是尝试获取锁,也就是尝试获取一下,成就成,不成就走,也不会阻塞。
ReentrantLock#tryLock(带参数)
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
ReentrantLock还有一个带参数的tryLock方法 boolean tryLock(long timeout, TimeUnit unit),该方法和无参的tryLock方法一样,也是尝试获取锁,成就成,不成不会马上走,而是阻塞等待一段时间。timeout参数就是指定阻塞等待的时间,unit则是时间单位。
ReentrantLock#newCondition
public Condition newCondition() {
return sync.newCondition();
}
ReentrantLock的newCondition可以返回一个Condition对象,Condition 可以实现类似于和synchronized加Object#wait的功能,这个我们后面的文章再介绍吧。