ReentrantLock 的图解运行流程 (加锁、释放锁流程)
文章目录
其他相关文章
0、ReentrantLock , 了解大概的 概述、原理、流程:
ReentrantLock 是基于AQS实现的 可重入的互斥锁 , AQS(AbstractQueuedSynchronizer抽象类 - 抽象队列同步器)
AQS中维护了一个双向的链表 , 链表中每个元素都是一个 AQS中的Node节点对象(Node节点) , 然后AQS中维护了一个 int类型且是 volatile 修饰的 变量 state
这个state在 ReentrantLock中(state在不同类型的锁中功能不同,如读写锁)
如果 state == 0 则表示当前ReentrantLock还未被线程持有
如果 state == 1 则说明已经有线程持有了当前锁资源
如果 state > 1 说明持有锁的线程已经进行了锁重入ReentrantLock的实现中 , 当前锁资源只能够被一个线程持有(这点和synchronized相同-互斥锁) , ReentrantLock就是通过state的值来判断当前锁资源是否已经被持有
如果已经被持有,那么就需要加入到AQS维护的队列中去排队了 , 排队时就会执行一系列逻辑使当前线程挂起 , 直到当前锁资源释放到轮到唤醒自己时线程才会继续工作AQS刚开始的结构 , 其head和tail都是指向null的 , state为0
/**
* AQS 内部Node类 核心属性
*/
static final class Node {
// 共享节点 , 共享的在ReentrantLock中不涉及 , 他是互斥锁
static final Node SHARED = new Node();
// 互斥节点
static final Node EXCLUSIVE = null;
// 当前节点waitStatus的状态之一 --> 取消状态
static final int CANCELLED = 1;
// 当前节点waitStatus的状态之一 --> 信号状态 , 如果当前节点的waitStatus为-1 , 那么表示后继节点处于挂起状态
static final int SIGNAL = -1;
// 当前涉及不到
static final int CONDITION = -2;
// 当前涉及不到
static final int PROPAGATE = -3;
// 当前节点的状态 , 新建时默认是 0
volatile int waitStatus;
// 当前节点的前驱节点
volatile Node prev;
// 当前节点的后继节点
volatile Node next;
// 当前节点存储的线程对象
volatile Thread thread;
}
一、场景:
1.1 场景描述
现在模拟有 四个线程在使用 Lock锁 进行加锁执行业务逻辑 , 然后分析每个线程执行加锁流程图解
线程T1
: 目的是为了一直占用着锁资源
线程T2
: 普通的等待节点
线程T3
: 使用Lock的tryAcquire(time,unit) 方法让其挂起10s后尝试获取锁资源
由于T1线程一直持有着锁资源 , 所以T3线程最终会执行cancelAcquire方法
线程T4
: 普通的等待节点 , 这个线程用来观察T3取消后时 AQS内的状况
1.2 场景源代码
public class ReentrantLockBlog{
private static Lock testLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
mainCode();
}
/**
* 创建四个模拟线程
*
* T1 : 让第一个线程一直占用着锁资源
* T2 : 普通的等待节点 让其一直等待
* T3 : 使用tryAcquire(time,unit) 让其挂起10s后尝试获取锁资源 , 由于T1一直占用锁资源 那么T3线程最后会执行 cancelAcquire 取消节点
* T4 : 也是一个普通节点 , 为了在Debugger时看到 cancelAcquire的节点
* T5 : 普通节点
*/
public static void mainCode() throws InterruptedException {
// T1
Thread t1 = new Thread(() -> {
testLock.lock();
try {
handleProcess();
sleepThread(TimeUnit.SECONDS , 20);
}finally {
testLock.unlock();
}
},"T1");
t1.start();
sleepThread(TimeUnit.MILLISECONDS , 10);
// T2
Thread t2 = new Thread(() -> {
testLock.lock();
try {
handleProcess();
}finally {
testLock.unlock();
}
}, "T2");
t2.start();
sleepThread(TimeUnit.MILLISECONDS , 10);
// T3
Thread t3 = new Thread(() -> {
boolean tryLock = false;
try {
if (testLock.tryLock(10 , TimeUnit.SECONDS)) {
tryLock = true;
handleProcess();
}else {
System.out.println("tryLock(time,unit) 方法没有拿到锁资源 并且没有剩余休眠的时间 自动唤醒");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (tryLock){
testLock.unlock();
}
}
} , "T3");
t3.start();
// 让T3节点执行取消方法 时加入新节点T4 , 保证T3节点不是tail否则将直接移除了
sleepThread(TimeUnit.SECONDS , 5);
// T4
Thread t4 = new Thread(() -> {
// 在 testLock.lock(); 这行打Debug即可看到此时的AQS队列情况
testLock.lock();
try {
handleProcess();
}finally {
testLock.unlock();
}
}, "T4");
t4.start();
// T5
Thread t5 = new Thread(() -> {
testLock.lock();
try {
handleProcess();
}finally {
testLock.unlock();
}
}, "T5");
t5.start();
}
private static void sleepThread(TimeUnit unit , long sleepLong){
try {
unit.sleep(sleepLong);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "抛出中断异常!");
}
}
/**
* 处理业务代码
*/
private static void handleProcess(){
System.out.println(Thread.currentThread().getName() + "拿到了锁资源!");
System.out.println("处理业务代码!");
System.out.println("业务代码处理完成!");
}
}
/*
输出结果:
T1拿到了锁资源!
处理业务代码!
业务代码处理完成!
tryLock(time,unit) 方法没有拿到锁资源 并且没有剩余休眠的时间 自动唤醒
(T1一直在睡眠所以程序不会结束 要一直等待T1睡醒)
*/
二、AQS中的关键点
2.1 hasQueuedPredecessors 方法含义
/**
* 该方法就是判断 当前时刻 AQS队列中 , 是否有线程在排队 , 有在排队则返回true
* 这个方法最重要的就是配合 当前线程是否具有抢占锁资源资格 ,
*
* **从代码角度来看** :
* 1. h != t
* 说明 head和tail已经初始化 , 如果没有初始化则说明当前AQS队列肯定没有排队的 返回 true
* 2. (s = h.next) == null || s.thread != Thread.currentThread())
* 说明不存在head的下一个节点 或者 下一个节点不是当前线程 那么就返回 true
* **配合tryAcquire来看** :
* 1表示没有线程排队则直接返回false让tryAcquire里可以进行CAS操作尝试获取锁资源
* 2表示队列已经初始化了 但是 head的next节点(第一个有效节点) 是当前线程 则返回false, 那么也可以尝试获取锁资源
*/
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
2.2 cancelAcquire 取消节点方法 (只针对当前场景代码解释)
这里不对cancelAcquire方法做详解 , 详细源码解释看 其他博客
1. 真正将取消节点移除AQS中的情况如下
1.当取消的是tail节点时
: 此时可以认为node直接被清理出AQS中了
2.当取消的是head的next节点时
: 则唤醒取消节点的next节点即 head的next的next节点 , 此时这个唤醒的节点就成为了 head , 使用setHead方法将 取消的节点移除AQS中
3.取消的是AQS的中间节点
: 个人认为是只有当 prev是取消节点 的这个节点拿到锁资源 同样是调用setHead方法后 才会将其prev取消节点真正移除出AQS队列
至于 shouldParkAfterFailedAcquire 方法真正起作用的地方 个人觉得条件会比较苛刻(少见)
因为 如果取消的是中间的节点并且还需要 取消节点的后继节点刚刚加入到AQS队列(addWaiter方法) 并且还未调用shouldParkAfterFailedAcquire时刻 , 等取消的节点操作完毕后 shouldParkAfterFailedAcquire才执行这样才能
真正意义的把 当前取消的节点真正移除出去
2. 总结: 真正移除取消节点的方式
1.shouldParkAfterFailedAcquire 在cancelAcquire方法后才执行 , 并且保证取消的节点不是tail
2.prev是取消节点 的节点拿到锁资源后 setHead 方法会移除取消的节点
真正取消节点是在 某个节点唤醒时 其尝试获取锁资源成功然后调用setHead时 才会真正取消掉节点
private void setHead(Node node) {
head = node;
node.thread = null;
// 就是这里 , 会把当前节点 prev关联的已经处于取消状态的节点真正移除AQS队列中
node.prev = null;
}
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
/**
* 1.这个while循环 将当前需要进行取消的节点node 节点的prev指针指向前驱节点状态不是取消的节点(离node最近的)
* 在当前代码的情景下 node的前一个节点不是取消状态的 所以不会执行这个
*/
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
// 此时取消的节点已经没有任何引用指向node 就等待GC把node清除 所以认为是直接移除出AQS队列了
compareAndSetNext(pred, predNext, null);
}
else {
int ws;
// 由于T3是在AQS的中间位置,而不是两边所以走这个if逻辑
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 2. 将node的前驱节点(T2) 的next指针指向当前node的next节点(T4) , 此时 从head往tail方向将不可达当前node(T3)了
// 但是此时 取消节点node的prev指针还是可以遍历到的 从tail到head方向
// 因此执行了cancelAcquire方法并不会立即将node节点清理出AQS队列中
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
// 3. 取消节点node的next指针指向自己
node.next = node; // help GC
}
}
三、加锁
前言
: 先看一下场景源代码 知道大概背景和意图 场景源代码
逐步分析
:
-
1. 创建Lock锁对象时
:private static Lock testLock = new ReentrantLock();
- 此时 AQS 内部的状态就是这样的
- 此时 AQS 内部的状态就是这样的
-
2. 当T1线程开始执行任务时
:- 此时 AQS 中 state 属性是0 , 那么T1线程会直接去尝试获取锁资源, 拿到锁资源之后就会一直持有着锁资源直到睡眠时间结束
- 创建完成Lock锁之后 如果仅有一个线程去获取锁资源 那么AQS队列是不会做任何变化的 不会初始化更不会去排队 一直都是 刚创建Lock锁时的状态图 (公平锁、非公平都是这样的)
-
3. 当T2线程开始执行任务时
:- 此时当前线程使用
testLock.lock()
, 发现AQS中state属性不是0 , 并且尝试获取锁资源失败 , 那么就会把当前节点加入到AQS队列中,
addWaiter方法内有一个enq方法,这个方法是进行初始化AQS队列生成head伪节点和 保证本次CAS替换tail成功的目的 , 所以会先进行初始化AQS队列
然后就会把当前节点里边的线程挂起 并把前驱节点的 waitStats设置为-1
-
- 此时当前线程使用
-
4. 当T3线程开始执行任务时
:- tryLock(time,unit) 方法作用是将当前线程封装为Node节点放入AQS队列中挂起指定时间后 尝试获取锁资源如果获取不到就取消这个节点
- 明显的T1一直占用着锁资源 T3线程挂起10s也拿不到锁资源 所以就会取消掉T3这个节点 , 取消在T4和T5线程都加入再说
-
5. T4、T5获取锁资源同样的
加入全部节点后的状态 -
6. 在5加入全部节点的基础上分析T3取消节点
-
由于T1线程一直占用着锁资源 T3启动后加入到AQS队列中等待10s后一定是拿不到所资源的
所以T3线程在自动唤醒之后会执行cancelAcquire(Node node) 方法 -
查看 cancelAcquire部分代码 的部分代码 , T3线程所在节点为 AQS队列中间部分(非两边)
-
T3线程经过取消方法后的AQS图
-
-
通过Debug线程T5里边可以看到
-
从head往tail 找不到T3节点
-
从tail往head 可以找到T3节点
-
当前边所有节点都唤醒、然后释放所资源之后 到T3成为head的next节点时 如果这时T4才加进来那么执行 shouldParkAfterFailedAcquire 才会将T3节点完全移除AQS队列
-
-
四、释放锁
前言
: 还是按照此场景代码来分析释放锁流程步骤
- tryRelease 代码
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
逐步分析
: 不要忘记此时T3已经是取消的节点了
-
当T1休眠20min后 调用 testLock.unlock()方法
- unlock方法在这里不再详解
-
当T2释放锁资源时的流程图