Java 除了使用关键字 synchronized 外,还使用了 ReentrantLock 实现独占、可重入锁的功能。相较于 synchronized,ReentrantLock 更为丰富和灵活。除此之外,我们或多或少还听说过一个名词 AQS。AQS 是Java 提供的底层同步工具,用一个 int 类型的变量表示同步状态,提供一系列的 CAS 操作来管理这个同步状态。我们今天的主角 ReentrantLock 也是基于 AQS 实现的。好的,废话少说,我们开始源码的探索路程。
构造函数
无参构造,通过无参源码可知,默认情况下是创建的非公平锁。
也就是说 ReentrantLock lock = new ReentrantLock();这是非公平的。
public ReentrantLock() {
sync = new NonfairSync();
}
那么如何调用公平锁呢?
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从源码得知,如果我们传了一个 true 参数,就会调用公平锁。
ReentrantLock lock = new ReentrantLock(true);这是公平锁。
属性
首先啊,我们打开源码就看到了 ReentrantLock 实现了 Lock 接口,那么 Lock 里面的一些加锁啊解锁啊等方法自然也要一一实现。
public class ReentrantLock implements Lock, java.io.Serializable {
其实ReentrantLock 就一个属性,那就是 sync。但 ReentrantLock 自己实现了一个 Sync 的静态内部类。这个 Sync 继承了 AbstractQueuedSynchronizer,也就是我们常说的 AQS。所以 AQS 中的一些重要属性自然而然也被ReentrantLock 继承。
abstract static class Sync extends AbstractQueuedSynchronizer {
进入 AbstractQueuedSynchronizer,我们依然可以看到里面也实现了一个静态内部类 Node。这个 Node 至关重要,它会出现在接下来的所有内容中。这次我只列举了ReentrantLock 需要用到的属性。
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
/** 共享模式 */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
/** 独占模式 */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
/** 表示当前节点处于 取消 状态 */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
/** 表示当前节点需要唤醒后继节点 */
static final int SIGNAL = -1;
/** node 节点的状态 */
volatile int waitStatus;
/** 因为需要构建双向链表,所以 prev 表示指向 node 节点的前一个节点*/
volatile Node prev;
/** 指向 node 节点的后继节点*/
volatile Node next;
/** node 封装的线程 */
volatile Thread thread;
/** 阻塞队列的头节点 头节点对应的都是当前线程*/
/** 这里的 head 并不是阻塞队列双向链表的 head,可以把 head.next 当做阻塞队列的开始
* 我在这里就犯了迷糊,在空队列的定义上犹豫了一会,所以这里给大家提个醒。
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
/** 阻塞队列的尾节点 */
private transient volatile Node tail;
/**
* The synchronization state.
*/
// 表示资源
// 独占模式:0 表示未加无锁状态,>0 表示加锁状态
private volatile int state;
}
ReentrantLock 里面需要用到的属性就大致讲完了,其实还有一个状态码为 0 的,它代表当前节点是新建的节点。哦,对了,AbstractQueuedSynchronizer 还继承了一个抽象类 AbstractOwnableSynchronizer,里面的exclusiveOwnerThread 属性及其 get、set 方法也是重点对象。
// 独占锁模式:表示当前独占锁线程
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
介绍完属性,接下来开始进入源码的阅读,这次主讲公平锁。先大致列举一下加锁阶段的方法。
lock()
|–>acquire() 竞争资源
|–> tryAquire() 尝试获取锁
|–> acquireQueued() 挂起当前线程
|–> shouldParkAfterFailedAcquire()// 当前线程获取锁资源失败后是否需要挂起呢?
true: 需要; false 不需要
|–> parkAndCheckInterrupt() 挂起当前线程,唤醒之后返回当前线程的中断标记
| –> addWaiter()将当前线程封装成node
OK,大致骨架梳理完了,源码开始,gogogogo!!
acquire(int arg) – 加锁
// 公平锁入口,不响应中断的加锁
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
// 尝试获取锁,成功返回 true,失败返回 false.
/* addWaiter 将当前线程封装成 node 入队
* acquireQueued 挂起当前线程
* 返回 true 表示挂起过程中线程被中断唤醒过
* 返回 false 表示未被中断过
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其中,acquire() 就是竞争资源的方法,首先回去尝试获取锁,成功了就执行其业务逻辑,不成功会将其包装成Node进入阻塞队列中并挂起。
tryAcquire(int acquires) – 尝试获取锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// AQS state
int c = getState();
// state == 0 表示当前 AQS 处于无锁状态
if (c == 0) {
/**
* 条件1:hasQueuedPredecessors()
* true:当前线程前面有等待者,当前线程需要入队等待
* false:当前线程前面无等待者,直接尝试获取锁
* 条件2:通过 CAS 的方式设置 state
* success:当前线程抢占锁成功
* fail:存在竞争,且当前线程竞争失败
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//设置当前线程为独占者 线程
// 注意:这里没有设置阻塞队列的 head 节点,后续在分析 addWaiter 的 enq 会提到
setExclusiveOwnerThread(current);
return true;
}
}
// c != 0 需要检查当前线程是不是 独占锁的线程,因为ReentrantLock 是可重入的
else if (current == getExclusiveOwnerThread()) {
// 更新值
int nextc = c + acquires;
// 越界判断
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 1.CAS 失败 c == 0 时,CAS 修改 sate时未抢过其他线程
// 2. c > 0 且 ownerThread != currentThread.
return false;
}
回到 acquire方法,tryAcquire 如果返回 true 取反后为 false,就返回业务层面了。可如果tryAcquire 为 false,取反后为 true,那么就要进入阻塞队列挂起了。接下来我们看一下 addWaiter()。
入队
private Node addWaiter(Node mode) {
// 将当前节点封装到 node 中
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 快速入队
// 条件成立:表示队列里面有 node
if (pred != null) {
// 将node 的 prev 指向 pred
node.prev = pred;
// 通过 cas 的方式将 node 入队
if (compareAndSetTail(pred, node)) {
// 前置节点指向 node,完成双向绑定。
pred.next = node;
return node;
}
}
// 什么时候会执行到这里?
//1. 当前队列是空队列 tail == null
//2. cas 竞争入队失败
// 完整入队
enq(node);
return node;
}
其实,addWaiter 方法无非就是将当前线程封装成 node节点加入到阻塞队列队尾中。如果一切进展顺利即上面所说的“快速入队”。如果中间出现阻塞队列为空队列或者 cas 插入队尾失败的情况则进入完整入队。接下来我们分析一下完整入队 enq()。
private Node enq(final Node node) {
/** 自旋入队,只有当前 node 入队成功后,才会跳出循环*/
for (;;) {
Node t = tail;
/*
* 队列为空队列
* 说明当前锁 被占用,且当前线程有可能是第一个获取锁失败的线程
* (为什么是有可能?因为当前时刻可能存在一批获取锁失败的线程.)
*/
if (t == null) { // Must initialize
// 作为当前持锁线程的第一个后继线程
// 1.因为当前持锁线程在获取锁时(tryAcquire),成功了,并没有向阻塞队列中添加
// 任何元素,所以这里要通过 cas 方式给阻塞队列添加头节点。
// 2.自己追加节点
// 然后我们便开始进入下一轮循环,下一轮直接进入 else
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 由于阻塞队列中已经有我们之前创建的节点了,所以我们直接将node的 prev指向 tail
node.prev = t;
// 通过cas的方式将当前 node 设置成新的尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
竞争逻辑
shouldParkAfterFailedAcquire 当前线程获取锁资源失败后是否需要挂起呢?true:需要挂起;false:不需要。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前置节点的状态 (前面有写,建议翻看)
int ws = pred.waitStatus;
// 成立:前置节点是可以唤醒当前节点的,返回 true 后,parkAndCheckInterrupt park当前线程
// 普通情况下,第一次来到 该方法 ws 不会是 -1
if (ws == Node.SIGNAL)
return true;
// ws > 0 表示前置节点是 CANCELED 状态
if (ws > 0) {
// 唤醒 canceled 节点的条件是当前节点的前置节点的 ws 为-1
// 那么这个 do while 循环就不断地向前找状态不大于 0 的节点
// 那么 ws > 0 的节点会出队(下面有图,可以参考一下)
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果当前 node 前置节点的状态为 0,
// 则会将当前线程 node 前置节点状态强制设置为 SINGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
下图是找唤醒节点的过程。(太丑了,没办法)
总结:
当前节点的前置节点是取消状态,第一次来到这个方法时会越过取消状态的节点,第二次会返回 true,然后 park 当前线程。
当前节点的前置节点状态是 0,当前线程会设置前置节点的状态为 -1,第二次自旋来到这个方法时,会返回true 然后 Park 当前线程。
接下来是 parkAndCheckInterrupt()
它是 AQS 的方法,park 当前线程 将当前线程挂起,唤醒后返回当前线程是否为中断信号唤醒。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
加锁总结
以上就是这篇文章的全部内容了,最后在概述一遍做个总结吧,主要是想梳理一下所有的源码知识点,毕竟无论是面试还是与人讨论我们都要大致说一下,总不能上来就滴水不漏地将整体细节全部描述出来。
- 如果我们用的是公平锁,那么我们就要调用FairSync 的 lock() 。
- 然后 lock 调用 acquire,参数传入 1,设置 AQS的 state 的值。
- 进入 acquire 我们首先要尝试拿到锁(tryAcquire),怎么去尝试拿到锁呢?我们先是获取当前线程,通过当前线程获取当前线程的状态值(state),如果状态值为 0,说明现在是无锁状态。然后判断当前线程前面有没有线程在等待(hasQueuedPredecessors),如果没有那么就通过 cas 的方式去设置 state值(compareAndSetState)。如果设置 state 值成功了,说明我们抢占锁成功了。把当前线程设置成独占者线程(setExclusiveOwnerThread),然后就返回 true 了。如果锁重入了呢,更新 state 值,再次设置新的 state,最后返回 true;
- 如果没有拿到锁,那么我们会将当前线程封装成一个 Node 节点,然后将这个节点添加到阻塞队列中。
- 进入到阻塞队列中我们还要看看当前线程是否是 park 状态,如果不是那么就将其挂起。并且唤醒线程的逻辑也在这个方法里。但唤醒的逻辑要下次再讲。到此整个加锁的过程全部完成。