概述
ReentrantLock 是 Java 并发包中提供的一种可重入锁,它比 synchronized 更加灵活,可以支持公平锁和非公平锁,同时还提供了一些高级功能,例如可中断、可限时等特性。
属性和方法(重点)
属性
/**
* ReentrantLock 中的一个内部类,而这个类继承自 AbstractQueuedSynchronizer ,也就是非常有名的
* AQS ,我们的许多加锁和解锁等操作都是通过这个类来实现
*/
private final Sync sync;
方法
/**
* 加锁的方法
*/
public void lock();
/**
* 解锁的方法
*/
public void unlock();
ReentrantLock 里面一眼看去其实核心的就是这个几个属性和方法,当然,这只是第一眼看,如果要分析的话,Sync 这一个类我们就够分析的了,这还得从 AQS 说起,所以这次我们以另一种的方式,进行一次加锁解锁的操作,来看看它的内部是怎么调用的。
流程图
属性和方法详解
lock()
public void lock() {
sync.lock();
}
首先看看我们的 lock() 方法,非常简单,就是调用了 sync 属性的 lock 方法。
sync.lock()
这里大家就需要注意了,我们的 sync 有两个实现,从名字大家可以得知,一个是公平锁,而一个是非公平锁。这里我们来看一看他们的实现有什么不同。
// 公平锁的sync的lock方法
final void lock() {
acquire(1);
}
// 非公平锁的sync的lock方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先我们看下他们的共同之处是都调用了 acquire()
方法,但是我们的非公平锁是在第一次 if
语句判断失败以后才会取走 acquire()
方法,而这个 compareAndSetState()
方法其实就是直接使用我们 CAS 的方式去对我们 AQS 中的 state
变量进行修改,如果修改成功,那么就直接把拥有锁的线程改为当前线程 setExclusiveOwnerThread()
。这也就是我们非公平锁和公平锁的区别,公平锁会在加锁的时候直接进行锁资源的竞争。接下来我们就看看 acquire()
方法做了什么。
acquire()
// 核心acquire arg = 1
public final void acquire(int arg) {
//1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法。 没有拿到锁资源,
// 需要执行&&后面的方法
//2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,
// 并且插入到AQS的队列的末尾,并且作为tail
//3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源
// 如果没在前面,尝试将线程挂起,阻塞起来!
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到,acquire()
方法里面只是几个方法的调用,但是这里每个方法都至关重要,代码中的注释已经大致给出了每个方法的作用,现在我们就来详细看下这几个方法。
tryAcquire()
首先就是 tryAcquire() 方法,这里又是两个实现,一个公平锁,一个非公平锁,我们一个一个来看。
其实不管是公平还是非公平,tryAcquire()
都是在尝试获取我们的锁资源
非公平锁
// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 拿到 AQS 的 state 的值
int c = getState();
// 如果state == 0,说明没有线程占用着当前的锁资源
if (c == 0) {
// 没人占用锁资源,直接进行抢占,不会进行任何排队的操作
if (compareAndSetState(0, acquires)) {
// 将当前占用这个互斥锁的线程属性设置为当前线程
setExclusiveOwnerThread(current);
// 返回true,拿锁成功
return true;
}
}
// 当前state != 0,说明有线程占用着锁资源
// 判断拿着锁的线程是不是当前线程(锁重入)
else if (current == getExclusiveOwnerThread()) {
// 将state再次+1
int nextc = c + acquires;
// 锁重入是否超过最大限制
// 01111111 11111111 11111111 11111111 + 1
// 10000000 00000000 00000000 00000000
// 抛出error
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 将值设置给state
setState(nextc);
// 返回true,拿锁成功
return true;
}
return false;
}
公平锁
// 公平锁实现
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 拿到 AQS 的 state 的值
int c = getState();
// 如果state == 0,说明没有线程占用着当前的锁资源
if (c == 0) {
// 判断是否有线程在排队,如果有线程排队,返回true,配上前面的!,
// 那会直接不执行返回最外层的false
if (!hasQueuedPredecessors() &&
// 如果没有线程排队,直接CAS尝试获取锁资源
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;
}
两个方法唯一不同的地方就是公平锁会在抢占之前判断是否有线程排队,而我们的非公平锁是直接进行的抢占。
addWaiter()
接下来我们看,如果 tryAcquire
方法没有抢占到锁,返回 false
,配合上我们前面的 !
,我们将继续执行后面的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,首先执行的就是 addWaiter()
,会将没有获取到锁资源的线程封装为 Node 对象,并且插入到 AQS 的队列的末尾,并且作为 tail 。
// 将当前线程封装为 Node 对象,并且插入到 AQS 队列的末尾
private Node addWaiter(Node mode) {
// 将当前线程封装为 Node 对象,mode 为 null,代表互斥锁
Node node = new Node(Thread.currentThread(), mode);
// pred是tail节点
Node pred = tail;
// 如果pred不为null,有线程正在排队
if (pred != null) {
// 将当前节点的 prev,指定 tail 尾节点
node.prev = pred;
// 以 CAS 的方式,将当前节点变为 tail 节点
if (compareAndSetTail(pred, node)) {
// 之前的tail的next指向当前节点
pred.next = node;
return node;
}
}
// 如果上述方式,CAS 操作失败,导致加入到 AQS 末尾失败,如果失败,就基于enq的方式添加到AQS队列
enq(node);
return node;
}
// enq,无论怎样都添加进入
private Node enq(final Node node) {
for (;;) {
// 拿到tail
Node t = tail;
// 如果 tail 为 null,说明当前没有 Node 在队列中
if (t == null) {
// 创建一个新的 Node 作为 head,并且将 tail 和 head 指向一个 Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
调用 addWaiter
以后,我们就可以获取到这个包装好的线程 Node。
acquireQueued()
acquireQueued()
方法会去判断当前的 Node 是否是 head 的 next 节点,如果是,就会尝试获取锁资源,如果不是,或者获取锁资源失败了,就会尝试挂起当前线程。
final boolean acquireQueued(final Node node, int arg) {
// 标识
boolean failed = true;
try {
// 无限循环
for (;;) {
// 拿到上一个节点
final Node p = node.predecessor();
if (p == head && // 说明当前节点是head的next
tryAcquire(arg)) { // 竞争锁资源,成功:true,失败:false
// 进来说明拿到锁资源成功
// 将当前节点置位 head,thread 和 prev 属性设为null
setHead(node);
// 帮助快速GC
p.next = null;
// 设置获取锁资源成功
failed = false;
// 不管线程中断。
return interrupted;
}
// 如果不是或者获取锁资源失败,尝试将线程挂起,并且确保当前节点的上一个节点状态正常
if (shouldParkAfterFailedAcquire(p, node) &&
// 通过 LockSupport 将当前线程挂起
parkAndCheckInterrupt())
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 确保上一个节点状态是正确的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 拿到上一个节点的状态
int ws = pred.waitStatus;
// 如果上一个节点为 -1
if (ws == Node.SIGNAL)
// 返回true,挂起线程
return true;
// 如果上一个节点是取消状态
if (ws > 0) {
// 循环往前找,找到一个状态小于等于0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将小于等于0的节点状态该为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里注意一下 shouldParkAfterFailedAcquire()
这个方法,它做了一个当前节点的上一节点的状态判断,比较需要注意的就是,如果节点状态为 1
,代表这个节点是取消节点,我们需要继续往前找到状态正常的节点,然后才将线程挂起。
这大概就是 ReentrantLock 的一个加锁流程,现在我们再来看看解锁。
unlock()
public void unlock() {
sync.release(1);
}
非常明显,调用的是我们 sync 的 release()
方法,所以我们直接来看 release()
方法。
release()
public final boolean release(int arg) {
// 核心的释放锁资源方法
if (tryRelease(arg)) {
// 释放锁资源释放干净了。 (state == 0)
Node h = head;
// 如果头节点不为 null,并且头节点的状态不为 0,唤醒排队的线程
if (h != null && h.waitStatus != 0)、
// 唤醒线程
unparkSuccessor(h);
return true;
}
// 释放锁成功,但是state != 0
return false;
}
protected final boolean tryRelease(int releases) {
// 获取state - 1
int c = getState() - releases;
// 如果释放锁的线程不是占用锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否成功的将锁资源释放利索 (state == 0)
boolean free = false;
if (c == 0) {
// 锁资源释放干净。
free = true;
// 将占用锁资源的属性设置为 null
setExclusiveOwnerThread(null);
}
// 将 state 赋值
setState(c);
// 返回 true,代表释放干净了
return free;
}
// 唤醒节点
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
// 如果头节点状态小于0,换为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿到当前节点的next
Node s = node.next;
// 如果s == null ,或者s的状态为1
if (s == null || s.waitStatus > 0) {
// next节点不需要唤醒,需要唤醒next的next
s = null;
// 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 经过循环的获取,如果拿到状态正常的节点,并且不为null
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
大致就是将我们的 state
-1,如果减到 0
了,那么就唤醒下一个节点,这里还需注意一点,我们在进行节点唤醒的时候是从尾部开找的,这是因为我们在添加节点的时候,是先将 prev 指针指向,然后将 tail 指向当前节点,最后才是将 next 指针指向 node,所以,如果从前往后来找,可能丢失某个节点,因为 next 的指向是最后才进行的。
结论
这就是 ReentrantLock
的核心源码,通过对 ReentrantLock
源码的深入解析,可以更加深入地了解 Java 并发编程的实现原理,同时也能够更加灵活地使用 ReentrantLock
提供的丰富功能。