ReentrantLock原理源码详解
Posted wen-pan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ReentrantLock原理源码详解相关的知识,希望对你有一定的参考价值。
I)、AQS介绍
要理解ReentrantLock,首先必须要理清楚AQS以及AQS是什么,有哪些重要属性,以及整个ReentrantLock的继承关系结构是什么样的。这部分对于第一次接触AQS来说却是有点绕。
1、什么是AQS
个人简单理解AQS,AQS(AbstractQueuedSynchronize)按照他的字面意思:
1、Abstract抽象的,说明该类是一个抽象类,具体一些如何控制同步策略需要子类去实现。
2、Queued队列,说明该同步器需要用一个队列来存放竞争锁失败的线程,以便于锁被释放的时候唤醒他们。
3、Synchronize同步的,说明该类是用来控制共享资源同步访问的。
2、AQS中最重要的几个属性
1、state:表示锁是否被占有(0表示当前没有任何线程占用锁,1表示该锁被某个线程占用)
2、exclusiveOwnerThread:表示当前持有锁的线程,如果没有任何线程占用锁,则该属性值为空。
3、head:表示未获得锁的线程等待队列的头节点
4、tail:表示未获得锁的线程等待队列的尾节点
3、AQS(AbstractQueuedSynchronizer)结构
通过类结构来展示AQS的主要属性:state、head、tail、exclusiveOwnerThread(在父类中定义)
AQS的exclusiveOwnerThread属性在他的父类中
II)、ReentrantLock
一、reentrantlock类图结构
可以看到在reentrantlock中组合了一个AQS属性(即sync),通过reentrantlock加锁解锁等相关操作其实都是通过调用AQS同步器相关方法来实现的。所以最主要还是要掌握AQS。
二、分析流程说明
为了清楚的演示竞争加锁、解锁的流程,本文按照如下顺序:
- 以非公平锁代码为例
- 假设首先有thread-0线程最先来获取锁,并且没有竞争,thread-0线程成功获取到锁。
- 然后thread-1线程又来获取锁,此时由于thread-0还并没有释放锁,所以此时thread-1需要被挂起等待。
- 然后thread-0释放锁,唤醒thread-1线程,然后thread-1线程开始竞争锁。
按照上面这三个步骤来一一分析加锁解锁流程和源码!!!
三、ReentrantLock加锁流程及源码
1、构造方法
通过构造方法可以看出,平时我们使用
ReentrantLock lock = new ReentrantLock();
的方式来创建锁的时候,默认创建的是一把非公平锁。
// ReentrantLock构造方法可以看到ReentrantLock默认的同步器使用的是非公平同步器(即ReentrantLock默认是非公平锁)
public ReentrantLock() {
sync = new NonfairSync();
}
2、thread-0线程加锁
假设这里thread-0线程调用
lock.lock()
方法来获取锁,并且此时锁没有被任何线程占有,则thread-0线程会成功获取到锁。通过的cas的方式将AQS的state由0设置为1并且设置ExclusiveOwnerThread为当前线程(以后可以通过ExclusiveOwnerThread
直接获取到持有锁的线程)。
public void lock() {
// 调用同步器的lock()方法加锁
sync.lock();
}
static final class NonfairSync extends Sync {
final void lock() {
// 通过cas的方式将state从 0 设置为 1 (cas是原子性操作,并发时只有一个线程会执行成功)
if (compareAndSetState(0, 1))
// 如果state设置成功,则设置该锁被当前线程独占(将当前线程标记为持有锁的线程)
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁失败的后续操作
acquire(1);
}
}
Thread-0线程加锁成功后非公平同步器状态
3、thread-1也来竞争锁
此时线程thread-1也来竞争锁了,Thread-1 执行了如下步骤
- Thread-1通过cas的方式尝试将 state 由 0 改为 1,结果失败
-
进入 tryAcquire 逻辑再次尝试获取锁,这时 state 已经是1,结果仍然失败
-
接下来进入 addWaiter 逻辑,构造 Node 队列,并将自己添加到等待队列中去
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的(即有竞争的时候才会创建)
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
4、thread-1竞争锁流程源码
①、调用lock()方法获取锁
static final class NonfairSync extends Sync {
// thread-1同样调用了lock方法来获取锁,此时cas获取锁失败,进入acquire方法
final void lock() {
// 步骤一、通过cas的方式将state从 0 设置为 1 (cas是原子性操作,并发时只有一个线程会执行成功)
if (compareAndSetState(0, 1))
// 如果state设置成功,则设置该锁被当前线程独占(将当前线程标记为持有锁的线程)
setExclusiveOwnerThread(Thread.currentThread());
else
// 步骤二、获取锁失败的后续操作
acquire(1);
}
}
public final void acquire(int arg) {
// 再次尝试获取锁,如果还是没有获取到(即 tryAcquire(arg) 返回为false),则将该线程加入等待队列
// tryAcquire(arg) : 再次尝试获取锁,获取成功返回true,失败返回false
// addWaiter(Node.EXCLUSIVE) : 添加到阻塞链表中
// acquireQueued : 将该节点挂起
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
②、tryAcquire方法
tryAcquire分为两大步骤:
①、首先判断AQS的state是否等于0(即未加锁),如果是则通过cas的方式将该值改为1,并且将
ExclusiveOwnerThread
设置为当前线程,表示获得了锁。②、如果state不等于0,则通过
exclusiveOwnerThread
去判断当前持有锁的线程是否是当前线程(这里就体现了exclusiveOwnerThread的用处了),如果是则将 state的值加一,表示重入锁。③、如果以上两种情况情况都不满足,则表示尝试加锁失败。返回false。
/**
* 非公平锁的尝试获取锁
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
// 入参 acquires = 1
// 获得当前正在运行的线程
final Thread current = Thread.currentThread();
// 获取state变量的值,即当前当前锁被重入的次数
int c = getState();
// ①、如果c=0,则证明当前锁没有被任何线程持有
if (c == 0) {
// 尝试自己通过CAS获取锁
if (compareAndSetState(0, acquires)) {
// 如果获取锁成功,则将当前线程设置为持有线程
setExclusiveOwnerThread(current);
// 返回获取锁成功
return true;
}
}
// ②、如果c != 0 并且 exclusiveOwnerThread = 当前线程,说明当前持有锁的是自己(该处对应重入锁)
// 如果当前线程就是锁的持有线程
else if (current == getExclusiveOwnerThread()) {
// 将state加一(即将重入次数加一)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state新值
setState(nextc);
// 返回锁的获取结果
return true;
}
// ③、否则返回获取锁失败
return false;
}
③、addWaiter方法
/**
* @return the new node
* 添加节点到等待链表尾部
*/
private Node addWaiter(Node mode) {
// 为当前线程创建一个节点,并将线程实例封装在该节点中,mode这里为null
Node node = new Node(Thread.currentThread(), mode);
// pred 指向 尾节点 tail
Node pred = tail;
// 如果尾节点不等于空
// 尝试快速入队
if (pred != null) {
// 将当前节点的prev指向尾节点,这里有可能多个node的prev指针都指向了这个最后的节点
// 如何解决呢?其实就是通过下面的 compareAndSetTail(pred, node) 原子操作,将AQS的tail指针指向这后面的多个节点中的一个
// 这里用cas保证只有一个节点能被指向.其他没有被指向的线程节点就会去执行下面的自旋入队逻辑 enq(node);
node.prev = pred;
// 通过cas的方式将尾节点的下一个节点next指向当前节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
// 返回当前节点
return node;
}
}
// 自旋入队,添加节点到双向链表尾部
enq(node);
// 返回当前节点
return node;
}
④、acquireQueued
-
acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
-
如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
-
进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1(-1表示该节点释放锁后需要唤醒他的下一个节点,当然这时head的waitstate = 0),所以这次循环返回 false,进行下一次循环。
-
acquireQueued进行下一次循环 ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
-
当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
-
进入 parkAndCheckInterrupt将当前线程阻塞(实际上调用的就是
LockSupport.park(this);
), Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
源代码:
/**
* 挂起节点
*/
final boolean acquireQueued(final Node node, int arg) {
// 失败标识
boolean failed = true;
try {
// 中断标识
boolean interrupted = false;
for (;;) {
// 获取当前新节点的前一个节点
final Node p = node.predecessor();
// 如果该节点的前一个节点是头节点,则说明很快就要轮到自己获取锁了,
// 所以在这里会再次调用 tryAcquire 再次去尝试一次获取锁
if (p == head && tryAcquire(arg)) {
// 如果获取到了锁,则将自己设置为头节点
setHead(node);
// 将p的next置为空,方便GC回收
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断该节点获取锁失败后(即上个if条件不满足时),是否应该被挂起到阻塞链表中(shouldParkAfterFailedAcquire)
// 如果 shouldParkAfterFailedAcquire 返回为true,则该节点被添加到阻塞链表中
// parkAndCheckInterrupt : 挂起节点
// 没有获取锁的节点需要挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果失败
if (failed)
// 取消获得锁,资源回收
cancelAcquire(node);
}
}
/**
* 判断一个节点在获取锁失败后,是否应该被挂起
* 注意:是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,
* 而不是本节点的waitStatus 决定
* 如果该节点的前一个节点的waitset为-1时该节点才能被挂起。其他情况都不能被挂起,为什么呢?
* 因为节点的waitset值 = -1 ,表示该节点释放锁后需要唤醒他的下一个节点,如果前一个节点的waitset值不等于-1
* 那么此时如果将这个节点挂起,那么该节点以后永远也不会被唤醒了。所以不能挂起
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 该节点的前一个节点的等待状态
int ws = pred.waitStatus;
// 如果前一个节点是正常等待唤醒状态,则返回true.说明该节点应该被挂起
if (ws == Node.SIGNAL)
return true;
// 前一个节点被取消或等待超时,此时需要循环的往前寻找,找到一个正常的节点,然后把当前节点加到正常节点后面
if (ws > 0) {
// 循环查找正常的节点,并且将该节点加入到正常节点后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 否则,将前一个节点的waitStatus设置为SIGNAL(SIGNAL=-1,代表正常状态,并且当该节点释放锁以后需要唤醒他的下一个节点)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
四、ReentrantLock释放锁流程及源码
①、释放锁流程
假设此时thread-0执行完任务了,需要释放锁,线程thread-0释放锁有两个步骤:
- 设置 exclusiveOwnerThread 为 null
- 设置state = 0
当前等待队列不为 null,并且 head 的 waitStatus = -1说明需要唤醒下一个节点,所以进入 unparkSuccessor 流程。找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 。Thread-1 被唤醒后继续竞争锁,进入 acquireQueued
流程。
情况一、如果此时 thread-1 竞争锁成功,则会设置:
exclusiveOwnerThread
为 Thread-1,state = 1- head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
情况二、如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了,如果不巧刚好锁又被 Thread-4 占了先,则:
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
②、释放锁源码
public void unlock() {
sync.release(1);
}
/**
* 释放锁
*/
public final boolean release(int arg) {
// 先尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 当前等待队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
if (h != null && h.waitStatus != 0)
// 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行
unparkSuccessor(h);
return true;
}
return false;
}
/**
* 尝试释放锁
*/
protected final boolean tryRelease(int releases) {
// 计算更新的state值
int c = getState() - releases;
// 释放锁的时候如果判断到当前持有锁的线程不是自己,则抛出异常(自己只能释放自己加的锁)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果c==0,则说明需要释放锁,直接将free置为true,并且设置当前独占线程为空
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新state状态(设置重入次数标志)
setState(c);
return free;
}
/**
* 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 找到队列中离 head 最近的一个 Node(没取消的)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// unpark 恢复其运行
LockSupport.unpark(s.thread);
}
以上是关于ReentrantLock原理源码详解的主要内容,如果未能解决你的问题,请参考以下文章
Java Review - 并发编程_独占锁ReentrantLock原理&源码剖析