ReentrantLock 顾名思义就是一把可重入锁,另外 ReentrantLock 还支持公不公平锁,默认是不公平锁。此外ReentrantLock 不涉及 AQS 中的共享锁,所以它还是一把互斥锁,所有的获取锁操作都互斥(除了锁重入),这个和 ReentrantReadWriteLock 有点区别,ReentrantReadWriteLock 读与读之间共享。它的基本功能和 synchronized 一样,但额外还提供一些别的功能,例如中断锁,超时锁。
ReentrantLock 的锁操作基于 AQS,如果你非常了解 AQS 那这篇文章对你来说会非常轻松。不了解 AQS 的请看 上一篇文章 AQS。还有本文不会对 AQS 做分析,只会对 AQS 中的模板方法,例如 tryAcquire,tryRelease 进行分析。
AQS state 成员变量在 ReentrantLock 中的意义:0 表示锁空闲,> 0 表示当前线程重入锁的次数。这也是 ReentrantLock 的关键。
首先我们看下 ReentrantLock 的基本构造:
public class ReentrantLock implements Lock, java.io.Serializable {
public ReentrantLock() {
sync = new NonfairSync();//默认是非公平锁
}
//指定公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
另外 ReentrantLock 有三个静态内部类 Sync,NonfairSync,FairSync。
1. 获取锁
public void lock() {
sync.lock();
}
sync 由构造方法确定。可能是公平锁也可能是非公平锁。
1.1. NonfairSync#lock
非公平锁的情况:
final void lock() {
if (compareAndSetState(0, 1))//CAS 修改 state 的值为1,如果修改成功说明目前锁空闲。
//把锁的拥有者改为当前线程。
//这里涉及到 AbstractOwnableSynchronizer 类,是 AQS 的父类,维护谁获取了当前锁
setExclusiveOwnerThread(Thread.currentThread());
else
//失败说明当前锁存在竞争,且竞争失败
acquire(1);//调用 AQS 的 acquire 方法
}
1.1.1. AQS#acquire
public final void acquire(int arg) {//在 ReentrantLock 里 arg 表示当前线程获取锁的次数增加一次
if (!tryAcquire(arg) && //tryAcquire 失败则进入同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//详情请看上一篇文章 AQS 分析
selfInterrupt();
}
tryAcquire 是尝试再次获取锁,如果成功则立刻返回成功,如果失败则把当前线程封装成 Node 并放入等待队列,并且阻塞当前线程,直到前面的 Node 唤醒自己(可能会 blocking and unblocking 多次)或者被其他线程中断。如果发现自己已中断,则自己再次中断自己。
就像我们在 AQS 分析时说的,tryAcquire 就是真正的获取锁操作。
1.1.2. NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
这里没有像公平锁一样把 tryAcquire
直接实现,而是使用 Sync#nonfairTryAcquire
,因为 ReentrantLock#tryLock
也会使用 nonfairTryAcquire
方法(无论它是公不公平锁)。
1.1.3. Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState(); //获取锁的状态值
if (c == 0) { //0 说明锁空闲
if (compareAndSetState(0, acquires)) {//CAS 获取锁
setExclusiveOwnerThread(current);//获取成功
return true;
}
}
//当前线程就是拥有锁的线程,则不需要获取锁,直接返回成功。锁的重入性也在这里体现
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;//锁的重入次数 + 1
//重入次数 > 2147483647 (二进制 1111111111111111111111111111111)就溢出。
//因为 + 1 之后符号位由 0 变成 1,变成了负数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置 state 这里没有使用 CAS 操作,因为当前线程已获得锁,此操作在锁里
setState(nextc);
return true;
}
return false;
}
这里我有个疑惑,为什么重入次数 > 2147483647后不反回 false 进入同步队列呢?
1.2. FairSync#lock
公平锁的情况:首先要明白公平锁就是对于每个线程获取锁他都是公平的,至少对于 ReentrantLock 来说满足 FIFO (先进先得),对于读写锁来说会有点区别,这里不做过多的讨论。
final void lock() {
acquire(1);//即 AQS 的 acquire 方法
}
明白了公平锁的含义,我想对于这里为什么没有像非公平锁一样没有首先获取一次锁操作很好理解。
这里我们主要看下获取锁操作 tryAcquire
1.2.1. FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//因为是独占锁,c == 0,说明是锁空闲
if (!hasQueuedPredecessors() && //查看队列中是否有 Node 存在,如果有就阻塞。
//这也是公平锁的体现,和非公平锁唯一区别的地方
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//c != 0 且当前线程就是占用锁的线程,锁重入的体现
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//重入次数 > 2147483647 (二进制 1111111111111111111111111111111)就溢出。
//因为 + 1 之后符号位由 0 变成 1,变成了负数
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置 state
return true;
}
return false;
}
可以看到,公平锁的获取/重入大体上和非公平锁是相同的,唯一不同的就是 hasQueuedPredecessors
方法,那我们就看下这个方法。
1.2.2. AQS#hasQueuedPredecessors
该方法主要作用就是:查询是否队列中有等待的 Node
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && //头 != 尾,如果头等于尾说明没有等待线程
((s = h.next) == null || s.thread != Thread.currentThread());//头下一个是 null 或 头下一个线程不是当前线程
}
何时会出现 h != t && (s = h.next) == null 的情况?
答:首先 s == t,h != t 为 true 后此时 s 正好获取锁
获取锁就讲到这里,另外 tryLock
,tryLock(long timeout, TimeUnit unit)
这里不做详细分析,相信大家看下源码没问题。
2. 释放锁
public void unlock() {
sync.release(1);
}
这里调用 AQS 的 release 方法,很显然 ReentrantLock 的解锁需要一次一次的释放之前加的锁,无法一次性解锁所有的锁,因为这里使用了 1 作为参数。
2.1. AQS#release
这是释放独占锁的方法,在 AQS 已详细讲过。
public final boolean release(int arg) {
if (tryRelease(arg)) {//模板方法,调用子类相应的方法
Node h = head;
//h == null 说明没有发生过竞争,自然也没有后继Node需要唤醒
//如果 h != null && h.waitStatus == 0 说明链表没有后继 Node,也就是说它自己是最后一个 Node。
//因为每次加入 Node 时会把前一个 Node 设为 -1,尾部 Node 没有后继 Node,自然 waitStatus 也不会被修改
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
如果 tryRelease 返回成功,说明锁已经释放,此时如果链表 != null 且 还有后继 Node 则唤醒后继 Node。至于如何唤醒,请查看 AQS 分析,这里我们主要看 tryRelease
方法时如何释放的。
2.1.1. Sync#tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//锁的次数 -1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//如果是最后一次解锁,则返回 true
free = true;
setExclusiveOwnerThread(null);//清除当前拥有锁的线程
}
//修改state,因为还在锁里面,并且 state 是 volatile 修饰的,所以可以不使用 CAS
setState(c);
return free;
}
可以看到 tryRelease
的目的就是把 state 计数 -1,直到减到 0,锁才是真正释放。这也是独占锁的重入的体现。
3. 另外 ReentrantLock 支持 newCondition 方法
详情请看 Condition
总结
ReentrantLock 最重要的两个方法就是 tryAcquire
和 tryRelease
,理解了这两个方法你就理解了大部分。希望对大家有所帮助,如果有错误请大家指正。