ReentrantrantLock底层实现原理及分析
一、类图
ReentrantrantLock 的类图如下所示,可以看到,它也是实现了 Lock 接口,和我们自定义不可重入锁的步骤是一样的,其内部也是维护了一个内部类同步器类 Sync,这个同步器类也是继承自 AQS,不过需要注意的是这里面的同步器类 Sync 是抽象的,有两个实现,一个是非公平锁实现 NonfairSync,一个是公平锁实现 FairSync。

二、非公平锁实现原理
2.1 加锁流程
先从构造器开始看,默认为非公平锁实现,如下代码,其中 NonfairSync 继承自 AQS。
public ReentrantLock() {
sync = new NonfairSync();
}先看加锁流程,如下的 lock() 方法,可以看到,方法内部调用的是NonfairSync 实现的 lock() 方法
public void lock() {
sync.lock();
}接下来我们看下NonfairSync 的 lock() 方法是如何实现的,如下图:


当没有竞争的时候,通过 CAS 将 state 由 0 改成 1,并把 owner 线程改成自己,如下图:

当第一个竞争出现时,如下图,此时owner 已经是 Thread0 了,此时来了一个 Thread-1,此时 Thread-1 还是执行 lock() 方法。

此时 Thread-1 的CAS 肯定是失败的,此时就进入 acquire() 方法,此方法的内容如下所示:

1、进入 tryAcquire 逻辑,这时 state 已经是 1,结果仍然失败。
2、接下来进入 addWaiter 逻辑,构造 Node 队列,如下图,会在 head 后面添加一个节点,这个节点是双向链表,即 head 指向一个 node 节点,node 节点又指向下一个 node 节点。

图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态,Node 的创建是懒惰的。
需要注意的是,首次创建 node 时会创建两个,其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程。只有第二个 Node 才会关联线程 Thread-1,因为它竞争失败了,因为它是第一个加入的,所以它指向了链表的尾部。其实这个就是我们的等待队列。
等到创建成功之后,当前线程就会进入到acquireQueued 逻辑,如下图,这个方法主要是在一个死循环中不断尝试获得锁,失败后进入 park 阻塞。

1、首先获取前驱节点,然后检查前驱节点,判断前驱阶段是否为头节点,如果是,说明 Thread-1 的这个 Node 是处于第二位的,那么它就有资格再次调用 tryAcquire() 方法再次获取锁。
2、如果还是没有获取到锁,此时就会进入到shouldParkAfterFailedAcquire 逻辑,这个方法会把当前线程节点的前驱节点的 waitStatus 改为 -1,改为 -1 的意思是它有责任唤醒它的后继节点,此时方法返回 false,如下图:

3、shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败。
4、当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true。
5、此时就会进入 parkAndCheckInterrupt(),即 Thread-1 被阻塞住,用灰颜色表示,如下图:

再次有多个线程经历上述过程竞争失败,就会变成如下的样子:

2.2 解锁流程
解锁流程,如下的 lock() 方法,可以看到,方法内部调用的同步器 AQS 的 release() 方法。
public void unlock() {
sync.release(1);
}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;
}假设此时 Thread-0 释放锁,就会进入到 tryRelease() 方法,如果返回 true,就证明释放成功了,就会把owner 线程设置为 null,并且把 state 设置为 0,如下图:

tryRelease() 方法返回 true 之后,还需要检查队列中的 head 是否为空,还需要检查 head 中的 waitState 是否不等于 0 ,若返回 true ,则需要调用 unparkSuccessor() 方法唤醒下一个节点。
unparkSuccessor() 方法是找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1,此时回到 Thread-1 的acquireQueued() 方法的流程,如下图:

如果加锁成功(没有竞争),会设置 exclusiveOwnerThread 为 Thread-1,state = 1;head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread;原本的 head 因为从链表断开,而可被垃圾回收。
如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了,Thread-4 并不是阻塞队列里面的线程,如下图:

如果不巧又被 Thread-4 占了先,那么 Thread-4 被设置为 exclusiveOwnerThread,state = 1;Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞。
三、可重入原理
接下来我们看下非公平锁的可重入的实现原理,我们看下他的获取锁和释放锁的相关代码,
获取锁最终调用的是 nonfairTryAcquire() 方法,传入的 acquires 就是 1,首先判断 state 的状态是否是 0,如果是 0 就代表还没有别人获取锁,然后我就去 CAS ,如果成功则设置 owner 设置为自己,然后返回 true。
如果是同一个线程下次又调用nonfairTryAcquire() 方法,此时就回去判断当前线程是否为 owner 持有的线程,如果是,则证明发生了锁重入,此时就是将 state 的值 ++,由 1 变成了 2,
static final class NonfairSync extends Sync {
// ...
// 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()) {
// state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}锁释放的时候调用的是 tryRelease() 方法,如下所示,此时就会把 state 的值 --,此时返回一个 false,表示我只是让 state -- ,并没有真正的释放锁,直到下面的这个 c 变成 0 了,才会释放锁,返回 true。
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
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;
}四、可打断原理
4.1 不可打断模式
在次模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。
我们都知道线程在知道自己没有办法获取锁的时候就会进入到 acquireQueued() 方法的循环内不断的去尝试,如果还不成功,就会进入到parkAndCheckInterrupt() 方法里面等待。
但是进入到 parkAndCheckInterrupt() 方法的线程可以被其他的线程调用它的 interrupted() 方法唤醒,如下面的parkAndCheckInterrupt() 方法,会返回一个 boolean 表示是否被打断过,但是interrupted() 方法还会清除打断标记,即下次再 park 还可以 park 住。
如果parkAndCheckInterrupt() 返回 true 了就会进入 if 块了,此时就会对打断标记做一个记录,设置值为 true,没有做额外的处理,接下来还是会继续 for 循环,获取不了锁还是会 park 阻塞。只有当你获取到锁以后,会把打断标记作为结果返回。
此时就会进入到selfInterrupt() 方法,重新产生一次中断。
static final class NonfairSync extends Sync {
// ...
private final boolean parkAndCheckInterrupt() {
// 如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// interrupted 会清除打断标记
return Thread.interrupted();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
// 还是需要获得锁后, 才能返回打断状态
return interrupted;
}
if (
shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()
) {
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
public final void acquire(int arg) {
if (
!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
// 如果打断状态为 true
selfInterrupt();
}
}
static void selfInterrupt() {
// 重新产生一次中断
Thread.currentThread().interrupt();
}
}4.2 可打断模式
可打断模式就是在调用acquireInterruptibly() 方法获取锁时可以被打断,它和上面的方法区别是当线程被唤醒的时候,即调用doAcquireInterruptibly() 时会抛出一个异常,打断 for 循环等待,即线程停止去等待锁
// Sync 继承自 AQS
static final class NonfairSync extends Sync {
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
// ㈠ 可打断的获取锁流程
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
// 在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常, 而不会再次进入 for (;;)
throw new InterruptedException();
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}五、公平锁实现原理
公平锁和非公平锁的区别主要就是在 tryAcquire() 方法的实现中,当 c==0 即还没有别人占这个锁时,先调用 hasQueuedPredecessors() 方法判断队列中是否有前驱节点,没有才回去竞争,即只能是老二节点线程才可以运行,非老二节点线程不能运行。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
// 与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
if (!hasQueuedPredecessors() &&
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;
}
// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// h != t 时表示队列中有 Node
return h != t &&
(
// (s = h.next) == null 表示队列中还有没有老二
(s = h.next) == null ||
// 或者队列中老二线程不是此线程
s.thread != Thread.currentThread()
);
}
}六、条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类就是 ConditionObject。
6.1 await 流程
假设一开始 Thread-0 持有锁,如下图所示:

接下来如果线程调用了ConditionObject 类的 await() 方法,它就会进入到ConditionObject 的 addConditionWaiter 流程,如下图:

addConditionWaiter () 方法会把调用的线程加入到条件变量里面的双向链表之中,并且把新的 Node 状态变为 -2,表示在条件变量中处于等待状态,如下图:


接下来会进入到AQS 的 fullyRelease 流程,释放同步器上的锁,如下图:

接下来会 unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功,如下图所示:

此时 Thread-0 的状态就被 park 阻塞了,如下图:

6.2 signal 流程
假设此时 Thread-1 要来唤醒 Thread-0,如下图:

signal() 方法的代码如下所示,首先进行健康检查,看调用方法的线程是否为锁的持有者,即只有 owner 才可以唤醒其他线程,接下来查找条件变量列表中的第一个元素,查找完毕之后调用 doSignal() 方法。

doSignal() 方法是取得等待队列中第一个 Node,即 Thread-0 所在 Node

此时的状态图如下所示:

获取完Node 之后,调用 transferForSignal() 方法,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1

接下来就是 Thread-1 释放锁,把 state 置为 0,设置 owner 为 null,唤醒等待队列中的下一个元素了,整个流程结束。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
解决Java变异出现错误No enclosing instance of type XXX is accessible
这牌你文章主要给大家分享解决Java变异出现错误,具体的饥饿绝方案请看下面文章的内容,需要的朋友可以参考一下,希望能帮助到你2021-09-09
MyBatis游标Cursor的正确使用和百万数据传输的内存测试
这篇文章主要介绍了MyBatis游标Cursor的正确使用和百万数据传输的内存测试,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2024-01-01


最新评论