JUC 深入 ReentrantLock
文章目录
一、ReentrantLock 和 synchronized 的区别
1. 核心区别
- ReentrantLock是个类 , synchronized 是Java提供的关键字 , 都是在JVM层面实现的互斥锁
- ReentrantLock 必须手动释放锁 , synchronized 是可以自动释放的
2. 效率区别
- 如果竞争比较激烈,推荐使用 ReentrantLock , 因为它不存在锁升级的概念。
而synchronized存在锁升级概念,如果升级到重量级锁,
synchronized是不存在锁降级的
3. 底层实现区别
- 实现原理不一样 , ReentrantLock 就是基于AQS实现的
synchronized 是根据不同的锁级别实现的
4. 锁的功能上的区别
- ReentrantLock 比 synchronized 有更全面的功能
- ReentrantLock 可以实现 公平锁和非公平锁 , synchronized 只能是非公平锁
- ReentrantLock 可以设置 等待锁资源的时间 , synchronized 只能等待CPU调度
5. 选择哪个
- 如果对并发编程特别熟练,推荐使用 ReentrantLock , 因为其功能更丰富
如果掌握一般般 , 使用 synchronized 更好点
二、AQS 概述
1. AQS是什么 , 字面意思理解AQS , 稍微看点源码
- AQS --> (AbstractQueuedSynchronizer.java 类) , AQS是JUC包下的一个基类 , JUC包下的很多功能都是基于AQS来实现的 , 例如现在讲的 ReentrantLock
- 阻塞队列 , 线程池 , CountDownLatch , Semaphore 等工具类 都是基于AQS来实现的
- Synchronizer ==> 安全的:
- AQS 中提供了一个 由 volatile修饰 , 并且采用CAS方式修改的 int 类型的 state 变量
- Queued ==> 队列
- AQS 中维护了一个 双向链表 , 有 head、tail 节点 , 每个节点都是Node对象
- Node 对象里 可以设置 锁的模式 共享的、互斥的 , 还有Node的状态
- 每个有效的Node对象 都需维护了一个 Thread 线程对象
- AQS 内部结构 和属性
Node 的核心属性
static final class Node {
// Node 的模式 , ReentrantLock为互斥锁为 EXCLUSIVE
// 而读写锁中的读锁 是共享锁 , 其Node模式为 SHARED
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/*
Node 的状态标志 , 除了下边的四种状态其实还有个初始状态为0
*/
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// Node 的状态 , 默认初始值为 0
volatile int waitStatus;
// 当前Node的前驱节点
volatile Node prev;
// 当前Node的后继节点
volatile Node next;
// 当前Node维护的Thread对象
volatile Thread thread;
}
三、加锁流程源码剖析
3.1 加锁流程概述
3.1.1 线程拿到锁资源的判断
- 哪个线程通过CAS将 ReentrantLock 的核心属性 state 从 0 ==> 1 , 就表示哪个线程拿到了锁资源
- 这个是非公平锁的流程
- 获取不到锁资源进入排队时 , 如果自己需要挂起 则需要通知前驱节点 将其 waitStatus ==> -1
- 获取不到锁资源进入排队时 , 如果自己需要挂起 则需要通知前驱节点 将其 waitStatus ==> -1
3.2 三种加锁的分析 (三个方法)
3.2.1 lock 方法
1. 执行 lock 方法后 , 非公平锁和公平锁的执行流程
// 非公平锁
final void lock() {
/**
公平锁 - 调用 lock 方法后 不管当前锁有没有被线程持有 , 先直接基于CAS的方式 将锁的状态 state 从 0==>1
*/
if (compareAndSetState(0, 1))
// 表示获取锁资源 成功
// 获取锁资源成功 , 会将当前线程设置到 exclusiveOwnerThread属性 , 代表是当前线程持有着锁资源
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁资源失败 , 尝试获取锁资源
acquire(1);
}
// =============================================
// 公平锁
final void lock() {
// 直接 执行 acquire 尝试获取锁资源
acquire(1);
}
2. 不管公平还是非公平,最后都是走的 acquire方法,这里分析 acquire方法
1. acquire() 方法
public final void acquire(int arg) {
/**
1. tryAcquire : 再次查看 , 当前线程是否可以尝试获取锁资源
!tryAcquire 说明依然没有拿到锁资源 , 如果truAcquire成功了-说明拿锁成功 则直接结束acquire方法
反之 - 再次尝试 还没有拿到锁资源 , 就要考虑将当前线程入队列等待了
2. addWaiter : 将当前线程封装为Node节点 , 并插入到AQS双向链表的结尾
3. acquireQueued : 查看当前节点有没有资格抢占锁资源(是否是队列中的第一个节点) , 如果有抢锁资格 那么就尝试获取锁资源
如果尝试了没抢到 , 或者根本没有抢占资格 则就尝试将当前节点 放入AQS队列中,并试图挂起线程
*/
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 如果没有拿到锁资源 , 并且当前节点不是第一个排队的节点 , 那么就中断当前节点维护的线程
selfInterrupt();
}
// 不进 if 说明拿到了锁资源 , 直接结束lock流程
}
2. tryAcquire : 尝试获取锁资源 , 具体实现是由公平锁和非公平锁来实现不同的逻辑
- 非公平锁实现逻辑
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取到当前的 state 属性
int c = getState();
/**
判断 state 属性 如果为 0 则恰好证明 -> 在进这个方法之前拿到锁的其他线程 恰好也已经释放了锁资源
那么就尝试将当前线程 设置为持有锁资源的线程
*/
if (c == 0) {
// 尝试获取锁资源
if (compareAndSetState(0, acquires)) {
// 设置当前线程为 拿去当前锁的线程
setExclusiveOwnerThread(current);
// 获取锁资源成功 , 成功返回
return true;
}
}
/**
如果锁资源还未释放 , 则判断 当前锁资源的持有线程是否为当前线程
如果是当前线程 , 则表示 锁重入操作
*/
else if (current == getExclusiveOwnerThread()) {
// 改变 state , 其实也即 改变当前锁重入次数
int nextc = c + acquires;
/**
如果增加了锁重入次数后 , nextc < 0 则说明 当前重入次数已经超过了最大值 , 则抛出错误
01111111 11111111 11111111 11111111 ==> Integer.MAX_VALUE
10000000 00000000 00000000 00000000 ==> -2147483648
*/
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 没有发生错误 , 则设置当前的 state 为重入计算后的 state
setState(nextc);
// 重入成功 返回 true
return true;
}
// 当前锁没有处于释放状态 , 并且也不属于锁重入 则直接返回false
return false;
}
- 公平锁实现逻辑
// 公平锁实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/**
* 1. hasQueuedPredecessors 查看AQS中队列有没有线程在排队
* !hasQueuedPredecessors 为 true 则表示 [没有线程排队或者排队的第一个是当前线程]
* 那么就可以去尝试获取锁资源了
*/
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;
}
- 非公平锁的 hasQueuedPredecessors 方法
/**
查看是否有线程在 AQS双向队列中排队
返回 false 说明没有线程排队 , 或者排队的第一个线程是当前线程
返回 true 说明有线程在排队
*/
public final boolean hasQueuedPredecessors() {
// 获取头尾节点
Node t = tail;
Node h = head;
// 头节点的下一个节点
Node s;
/**
1. 如果 头和尾是同一个对象 (h == t) , 则说明 当前没有线程在排队
所以 (h != t) 说明 当前队列中有排队的线程
*/
return (h != t) &&
/**
2. 到下边的逻辑 则说明 当前队列有线程在排队等待
这一串的逻辑表示 , 有线程在排队 , 那么就看排在第一名的是不是我
如果第一名是我 则间接的表示现在AQS中没有线程在排队了 返回 false
如果第一名不是我 则表示 AQS 中有线程在排队则 返回 true
*/
((s = h.next) == null || s.thread != Thread.currentThread());
}
- tryAcquire的结论:
- 非公平锁 不管当前锁资源有没有被占有 都会去抢一下
- 公平锁 , 如果当前队列中没有线程在排队、或者排队的第一个节点是自己 才会去抢
3. addWaiter : 将没有拿到锁资源的线程 放到 AQS 双向队列中 (没有公平和非公平一说了 , 都要走acquire方法的这个方法)
- addWaiter 方法
// 没有拿到锁资源 过来排队 , mode -> 表示当前锁类型 互斥锁、共享锁
private Node addWaiter(Node mode) {
// 将当前线程封装为 Node 对象
Node node = new Node(Thread.currentThread(), mode);
// 拿到当前 AQS 中的尾部节点
Node pred = tail;
/**
* 1. pred != null 说明当前 AQS 双向队列已经 初始化过了
* AQS 在第一次生成时 head 和 tail 都是指向 null 的节点
*
*/
if (pred != null) {
// 当前节点的 上一个节点 指向 现在AQS的尾节点 (即 链表的插入元素时做的操作)
node.prev = pred;
/**
* 通过 CAS 替换当前AQS的尾节点为 当前新增的 node 节点
* 这里 CAS 失败了也没关系 后边会走 enq方法
*/
if (compareAndSetTail(pred, node)) {
// 然后再把原来的尾节点的下一个节点指向 本次新增的节点
pred.next = node;
return node;
}
}
/**
* 2. enq 方法 确保当前AQS队列 head、tail 节点被正确初始化
* 如果已经初始化 ,即走上边if语句 那么这个方法的作用就是 死循环保证替换尾节点的CAS操作成功进行
*/
enq(node);
return node;
}
- enq 方法
// 1. 如果头尾节点还未初始化 保证 head 和 tail 节点正确初始化 ,
// 2. 如果已经初始化 则 保证 CAS操作尾部节点 正确被替换当前node节点
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 1. AQS中的头尾节点未初始化 那么执行初始化操作
if (t == null) { // Must initialize
// new Node() 是构建一个 伪节点 , 作为 head 和 tail
// 伪节点是为了 监控AQS中后续节点的状态
if (compareAndSetHead(new Node()))
tail = head;
}
// 2. 初始化过了 , 死循环 则保证通过CAS 替换尾节点成功
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- addWaiter 结论
- 如果AQS还没有初始化 head、tail节点 则会初始化
- AQS初始化时 头节点是一个 伪节点 , 用于后续节点的监控作用(在当前方法还不涉及此操作 , 后续会有的)
- 通过 enq() 方法 内部的死循环方式 , 一定会将当前接入的节点放到 AQS 的队列队尾
4. acquireQueued : 判断当前线程是否还能够再次尝试获取锁资源 , 如果不能获取锁资源 或者 有没有获取到锁资源 则就尝试将当前线程挂起
- acquireQueued 方法
/**
* 当执行到 acquireQueued 方法 , 则表示 当前线程没有拿到锁资源 , 并且已经将线程封装为Node添加到AQS队列中进行排队了
* 使用lock方法执行下来的操作 在此方法中 不需要考虑中断操作
* @param node 就是 addWaiter 的产物 , 即返回当前线程的Node节点
*/
final boolean acquireQueued(final Node node, int arg) {
// failed 默认没有拿到锁资源 (这个属性 真正起作用的是 使用 tryLock() 和 lockInterruptibly() 方法)
boolean failed = true;
try {
// 中断标记位
boolean interrupted = false;
for (;;) {
// 拿到当前node 的前驱节点 , 当前节点的上一个节点不可能是null
final Node p = node.predecessor();
/**
* 1.(判断当前节点是否有资格抢占锁资源) : 如果上一个节点是 头节点 , 那么当前节点就是第一位Node , 那么当前线程就尝试获取锁资源
* 不论是 公平锁 还是非公平锁 如果是排在第一位的Node 那么都可以尝试获取锁资源
*/
if (p == head && tryAcquire(arg)) {
// 进入 if 说明 当前节点是第一个节点 并且获取到了锁资源
// 那么当前节点在AQS中也就没有排队意义了 , setHead 将当前节点设置为新的伪节点
setHead(node);
// 清除原来的 head 节点 的 next 指针 , 帮助GC
p.next = null; // help GC
// 拿到锁资源 failed 变为 false
failed = false;
return interrupted;
}
/**
* 1. 到这里 - 说明当前线程没有资格抢占锁资源 , 或者没有抢到锁资源
* 2. shouldParkAfterFailedAcquire 是基于上一个节点的状态 , 来判断当前节点是否可以执行挂起操作
* 如果上一个节点状态是 SIGNAL(-1) , 那么就返回true , 如果不是则返回false , 继续下次循环
* 还有一种情况 , 如果前边都是 已经处于取消状态(CALCELLED ==> 1)的节点 那么就需要将当前节点移动到适合的位置[pred.waitStatus > 0](状态为-1或者0的节点后边)
*/
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
interrupted = true;
}
}
} finally {
/**
* 获取锁资源失败 或者抛出异常 才会执行此if语句 , 在 tryLock 和 lockInterruptibly 方法才会涉及到此逻辑
* 执行线程等待操作时 抛出异常才会走这里
*/
if (failed)
cancelAcquire(node);
}
}
- setHead 方法
// 当前节点的线程已经 拿到了锁资源 , 那么就把当前head 变为node节点 , 这个node就成为了新的**伪节点**
private void setHead(Node node) {
// 将 node 变为新的伪节点逻辑
head = node;
node.thread = null;
node.prev = null;
}
- 方法 shouldParkAfterFailedAcquire
/**
* 当前Node没有拿到锁资源 或者 没有资格竞争锁资源 , 那么就尝试看能不能挂起线程
*
* AQS双向链表中 , Node 之间有个很有趣的关系 , 比如到这个方法 当前的node想要挂起操作
* 但是需要确保 node 前边的节点是一个正常的节点(state != 1 , 即不是中断的状态) , 如果前边的节点不正常 那么node执行挂起之后,没有节点可以去唤醒这个node了
* 也即node将一直处于挂起状态
* 所以当前节点需要在AQS队列的队尾处向前 遍历
* 如果找到前边状态是 0 , 那么就通过 CAS 改变其状态为 -1
* 如果前边的节点状态是 SIGNAL==>-1 才行 , 然后把当前node移动到找到的这个节点后边
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
/**
* Node节点的状态:
* 1 : CANCELLED(取消) , 表示当前节点已经取消
* 0 : 默认的Node节点状态
* -1 : SIGNAL(信号) , 表示当前节点的后继节点可以直接挂起
* -2 : CONDITION(条件) : 当在使用 Condition进行await、signal操作 时才会涉及到的状态
* -3 : PROPAGATE(传播) : 在涉及到 共享锁时 才会使用的状态
*
* Node节点的状态 : 只要不是 CANNELLED(1) , 那么节点就是正常的
*/
// 获取前一个节点的 waitStatus 属性
int ws = pred.waitStatus;
// 只有上一个节点状态是 SIGNAL(-1) 那么当前节点可以执行挂起操作
if (ws == Node.SIGNAL)
return true;
/**
* 1. 上一个节点的状态 > 0 只能是 CANCELLED 取消的状态
* 那么这时 当前线程是不允许执行挂起的 , 否则将会出现永久休眠的状态
* 2. 所以需要想办法 , 将当前节点 前驱指针 移动到 不是 CANNELLED 状态的后边
* 所以就一直 往前循环找正常的Node(状态不是1的都行)
* 3. 找到后 将当前节点的 前驱指针 指向找到的符合要求的节点
*/
if (ws > 0) {
do {
node.prev = pred = pred.prev;
/**
// 上边代码也就相当于是
// 先获取 前一个节点的前一个节点
pred = pred.prev;
// 再把 当前节点的前驱指针指向 pred
node.prev = pred;
// 然后再判断本次找到的 pred 节点是否符合要求 (状态不是 CANNELLED(1 ==> 也即 > 0)) , 不符合继续往前找
// 最坏的结果就是 从尾节点一直找到 头节点(伪节点) , 伪节点的状态是不可能是取消状态的
*/
} while (pred.waitStatus > 0);
// 找到符合标准的 前驱节点 pred 后 将 pred的next后继指针指向 当前node节点
pred.next = node;
} else {
/**
* 到这个 else 说明前一个节点的状态 不是 SIGNAL(-1) 或着 CANCELLED(1) 而是 0 默认的状态
* 那么就表示当前节点是正常的 , 那么就通过CAS设置 这个前一个节点的状态为 SIGNAL 状态 , 可以满足后边的节点执行挂起状态
* 先结束 acquireQueued 里的 for(;;) 循环一次 再次进入就能看到前一个节点状态是 SIGNAL , 那么就可以执行挂起操作了
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
/**
* 这个方法可以确认 , 当前线程 是正常唤醒的 还是 被中断唤醒的
* 如果是中断唤醒 那么就返回true了 , 正常唤醒返回false
*/
return Thread.interrupted();
}
- LockSupport - park
/**
* LockSupport.park 方法 , 会通过 Unsafe类 通过native方法 将线程从 RUNNING 状态 转变为 WAITING 状态
* 所以需要使用 LockSupport.unpark() 方法来唤醒被park方法处于WAITING状态的线程
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
3.2.2 tryLock 方法
1. tryLock() , 仅仅是尝试获取锁资源 , 其他什么操作都不做
- tryLock
/**
* 公平锁和非公平锁 调用tryLock 都是这个方法
*/
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
- tryLock内部调用
/**
* 这里也就是 上边分析 lock 方法时 非公平所的 尝试获取锁资源的逻辑哦
* 1. 看state的值 如果是0 , 则使用CAS尝试获取当前锁资源
* 2. 如果state不是0 就看当前线程是不是持有锁资源的线程 , 执行锁重入操作
* 3. 如果没有抢到锁资源 或者 不是锁重入 那么直接返回false
*
*/
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()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- tryLock 方法结论
- 不论是公平锁还是非公平锁 , 调用 tryLock 后都按照 非公平锁的 抢占锁资源的逻辑执行
- tryLock方法 仅仅是 尝试获取锁资源 , 并不会加入AQS等操作
- 拿到就拿到了 , 拿不到也没关系 不对加入AQS队列中
2. tryLock(long time, TimeUnit unit)
- 概述
设置一个时间 在这个时间内 线程会一直在AQS队列中等待设置的时间
当时间到了之后再次尝试获取锁资源 如果拿不到 则直接返回false
或者在等待过程中如果线程被中断 - 则会抛出中断异常
- tryLock(long timeout,TimeUnit unit) 方法
/**
* 传入 时间长度
* 传入的时间最终都会转和时间单位换为 纳秒
*/
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
- tryAcquireNanos() 方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 判断当前线程的中断标记位是否正常 , 如果线程的中断标记位已经是true了那么就不能往下进行了 直接抛出中断异常
if (Thread.interrupted()){
throw new InterruptedException();
}
// 1. 线程没有处于中断状态
// 2. 那么就先尝试获取锁资源(注意区分公平锁和非公平锁) , 拿锁成功直接返回true
return tryAcquire(arg) ||
3. 如果tryAcquire拿锁失败 , 那么就需要等待指定时间
doAcquireNanos(arg, nanosTimeout);
}
- doAcquireNanos 方法
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 如果等待时间 <= 0 就相当于和调用 tryLock没区别了 , 直接返回false 拿锁失败
if (nanosTimeout <= 0L)
return false;
// 设置等待的 结束时间 , 纳秒单位
final long deadline = System.nanoTime() + nanosTimeout;
// 将当前线程扔到 AQS 队列中
final Node node = addWaiter(Node.EXCLUSIVE);
// 拿锁失败 , 默认 true , 即默认是拿锁失败的
boolean failed = true;
try {
for (;;) {
/**
* 这段代码在将 lock方法时 判断当前线程是否具有抢占锁资源资格 和挂起线程那块一样的 acquireQueued
* 如果在 AQS 中 当前node 是 head的next 直接抢锁 (具有抢占所资源资格)
*/
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
// 只有在这里是 代表拿锁成功的 , 否则 failed 一直都是失败的
failed = false;
return true;
}
// 计算 剩余的等待时间 , 主要在上一步 判断是否具有抢占资格会消耗时间
nanosTimeout = deadline - System.nanoTime();
// 判断是否还需要等待
// 如果发现上一步消耗的时间比较长 已经超过等待的结束时间 , 那么直接拿锁失败 返回false
if (nanosTimeout <= 0L)
return false;
/**
* shouldParkAfterFailedAcquire(之前也讲过的) : 根据上一个节点确定当前节点能否挂起线程 , 并把当前节点移动到可以使其挂起的节点后边
*/
if (shouldParkAfterFailedAcquire(p, node) &&
// spinForTimeoutThreshold = 1000L , 1000纳秒
// 如果剩余时间太短 , 那么就不用挂起了 , 太短的话 执行挂起的时间就已经结束等待了
nanosTimeout > spinForTimeoutThreshold){
/**
* 时间不短(足够) , 那么就将当前线程 挂起剩余的时间
* parkNanos 将线程处于 TIME_WAITING 线程会自动唤醒
* parkNanos 方法处理过的线程有两种唤醒方式
* 1. 时间到了 自动唤醒
* 2. 等待过程中 ,其中断标记位被改为了true , 那么也会唤醒 - (会抛出中断异常)
*/
LockSupport.parkNanos(this, nanosTimeout);
}
// 这里就是判断 当前线程 是不是被中断唤醒的 , 如果是 则抛出中断异常
if (Thread.interrupted())
// 证明是中断唤醒的
throw new InterruptedException();
}
} finally {
if (failed)
// 如果拿锁失败 , 那么就走把当前在AQS队列中 等待的当前节点node取消掉
cancelAcquire(node);
}
}
- cancelAcquire 取消某个节点操作
/**
* 当 AQS 队列中 有的节点内的线程 被中断了 或者 , 那么就会执行这个方法 执行取消操作
* 取消节点的整体操作流程:
* 1. 需要把当前取消的 node 节点的 thread 属性设置为null
* 2. 以当前需要取消的节点开始 , 向AQS队列前找 , 找到有效节点 (节点状态不是 1 取消的) , 找到后就把自己的指针和找到的节点关联上
* 然后后边才会取消自己操作 , 会把向前找过程中已经是中断状态的节点跳过去
* 3. 将当前节点的 状态 设置为 1 , 代表当前节点是 取消的
* 4. 然后将当前节点 脱离 AQS 队列时会有三种情况
* 4.1 当前 node 是 tail 节点 , 从node向前找到有效节点 后 直接将 tail 指向该有效节点即可 , 然后设置这个有效节点的 next 节点为 null
* 4.2 如果当前取消的节点是 head 的后继节点 , 那么就需要 去唤醒当前节点的 后继节点
* 4.3 如果取消的节点 不是尾节点 也不是 头节点的后继节点 , 那么就需要处理前边和后边的节点的指针指向问题
*
* @param node , 需要执行取消的node节点
*/
private void cancelAcquire(Node node) {
// 当前节点为 null 直接忽略即可
if (node == null)
return;
// 将当前取消节点的 thread 属性设置为 null
node.thread = null;
/**
* 这块代码 , 就是以当前node开始 向前找到有效的节点 , 跳过中间 已经是取消状态的节点
* pred: 最终表示的就是 有效节点 Node
*/
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext : 表示的就是上边找到有效节点后 , 原始的有效节点的后继节点 , 即 oldValue 快照
Node predNext = pred.next;
// 将当前取消节点的状态 设置为 CANCELLED ==> 1 , 取消状态
node.waitStatus = Node.CANCELLED;
// ======================================
// 下边的代码 就是 将当前 node 脱离AQS队列的逻辑
// ======================================
/**
* 这里即 是第一种情况 , 要取消的节点是尾节点 , 那么直接将 tail 指针指向有效节点即可
*/
if (node == tail && compareAndSetTail(node, pred)) {
// 将有效节点的 next 指针通过CAS的方式 设置为 null
// 有效节点的next指针的 原始值 就是 predNext
compareAndSetNext(pred, predNext, null);
}
// 到这里表示 取消的节点可能不是尾节点(不排除 CAS 设置尾节点并发失败了的情况) , 这里要讨论剩下的两种情况
else {
int ws;
// 不是head的后继节点
if (pred != head &&
/**
* 拿到上一个节点的状态 并且判断上一个节点的状态为 -1 , 则说明后面的线程可以执行挂起操作
* 如果上一个节点状态不是 -1 并且 也不是取消状态 , 那么就把其状态改为-1
*/
((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
&& pred.thread != null) {
/** 上面的这一串判断 - 都是为了避免后边的节点不能被唤醒 , 所以一定会把 当前有效节点的状态改为 -1 这样才能保证后边节点处于挂起状态的线程被唤醒 **/
// 不需要担心后边的节点没有挂起 还要执行唤醒操作 , 执行唤醒操作时会进行判断
/**
* 到这说明 pred 节点是有效节点
* 那么就 替换 pred 的 next 节点为 当前node节点的下一个节点 , 把当前节点绕过去
*/
// 当前节点的下一个节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 将当前 有效节点的 next 指针指向 当前node节点的下一个节点 , 跳过node自己
compareAndSetNext(pred, predNext, next);
} else {
/**
* 没有通过上边判断 - 这里说明 , 当前节点node 是head的后继节点 , 那么就尝试唤醒后继节点
* 这个方法到 锁资源释放再说 , 锁资源释放的时候也需要进行后续节点的唤醒操作 也是这个方法
*/
unparkSuccessor(node);
}
// 将当前节点的 下一个指针指向自己 , 变成不可达对象 , 帮助GC回收
node.next = node; // help GC
}
}
3.2.3 lockInterruptibly 方法
- 概述
这个方法和 tryLock(time,unit) 方法类似 , 只不过这个方法会直至等待拿到锁资源为止 , 除了被中断抛出中断异常而停止
- lockInterruptibly
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
- acquireInterruptibly
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
- doAcquireInterruptibly
/**
* 这个方法就是 和 tryLock(time,unit) 方法的唯一区别
*
* 这个方法 拿不到锁资源 , 就死等锁资源
* 只有等到所释放时被唤醒 , 或者是被中断唤醒
*/
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() , 就是判断是否正常唤醒的
parkAndCheckInterrupt())
// 不是正常唤醒的 就抛出 中断异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
四、释放锁流程源码剖析
4.0 释放锁的大概流程
- 释放锁资源 , 其实就是调用 tryRelease() 方法了
- 首先要判断是不是当前线程持有的锁资源 , 不是当前线程持有的锁资源让其去释放锁 不太合适 ,
- 不是的话就抛异常 , 阻止执行后边流程
- 是的话就执行逻辑 , 需要对 属性state - 1 操作
- 如果不是0 , 那么本次释放就结束了 方法结束
- 如果-1之后 state == 0 那么证明锁资源释放干净了(锁重入几次就需要释放几次)
- 释放干净之后 , 就先查看头节点(伪节点)状态 , 头节点的状态 waitStatus 是否不为0(是否为-1)
- 如果是0 那么就表示 AQS 队列中没有挂起的线程
- 如果是-1 那么就表示 后续 AQS 队列中 有挂起的线程 需要唤醒
- 唤醒线程时 , 需要先将当前节点的状态改变为0 , 然后去后边找有效的节点执行唤醒(不能去唤醒取消的节点 唤醒取消的节点没有意义) , 找到之后唤醒线程即可
- 寻找有效节点是 从后向前找的
4.1 unlock() , 释放锁操作 不区别公平锁还是非公平锁
- unlock
public void unlock() {
// 释放锁资源 不区分公平锁和非公平锁
sync.release(1);
}
- release
public final boolean release(int arg) {
// tryRelease 核心释放锁资源的操作之一
// 如果没有抛出异常 , 返回 true 则说明锁重入操作已经释放干净 , 如果返回false 说明锁重入还未释放干净
if (tryRelease(arg)) {
Node h = head;
/**
* h 如果是 null , 那么只能说明 AQS 根本就没有初始化 , 也就是当前锁资源 一直没有出现过排队的现象 一直都是一个线程在拿锁和释放锁 , 也就没必要去执行后边的操作了
* 反之 , 需要判断伪节点的状态是否是 -1 (ReentrantLock中只能出现-1了)
* 如果状态是-1 , 那么就需要执行唤醒操作了
* 如果没有那么就直接返回true 表示释放成功了
*/
if (h != null && h.waitStatus != 0)
// 执行唤醒后续节点操作
unparkSuccessor(h);
return true;
}
return false;
}
- tryRelease
// ReentrantLock 的释放锁资源操作
protected final boolean tryRelease(int releases) {
// 拿到锁资源 对state属性-1 , 拿到结果的快照 还并未写回
int c = getState() - releases;
// 判断当前持有锁的线程 是否是当前线程 , 如果不是就直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 代表当前锁资源是否已经释放干净了
boolean free = false;
// 证明释放干净了
if (c == 0) {
free = true;
// 设置持有锁的线程设置为 null
setExclusiveOwnerThread(null);
}
// 最后写回 state 属性
setState(c);
// 锁资源释放干净 返回true , 否则返回false
return free;
}
- unparkSuccessor , 唤醒后续节点方法
/**
* 唤醒后边排队的线程 (传入的节点一定是 head 节点 , 从head节点开始唤醒)
*
* 找有效节点的方式是从后往前找的 , 需要直到 addWaiter 添加节点的大致流程,就知道 添加时先把节点的prev指针指向原来的tail节点 , 然后把tail指向新增的node
* 这样可以避免出现 不能唤醒到新增的节点
*/
private void unparkSuccessor(Node node) {
// 拿到头节点的状态
int ws = node.waitStatus;
// 头节点的状态是 -1 , 那么就先将其状态改为 0
if (ws < 0){
// 通过CAS操作改变头节点的状态
compareAndSetWaitStatus(node, ws, 0);
}
// s : 头节点的后继节点
Node s = node.next;
// 如果后续节点是 null , 或者 后续节点的状态 > 0 即取消状态(1) , 就需要在AQS中找到有效节点去唤醒了
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找的原因 , 新进来的节点 在tail指向新节点之前 新节点的prev就已经指向了前一个节点 (可详看 addWaiter(node) 方法) , 这样会找到AQS中的所有节点
// 从前往后找的话 就有概率出现 不能被唤醒的节点
// **从后往前** 找到AQS中的有效节点 , 一直找到离当前node最近的有效节点 (从 t != node 看出来) , 将其给到 s
for (Node t = tail; t != null && t != node; t = t.prev)
/**
* 从后往前找,找到了离node最近的 并且是有效的节点 , 赋值给 s
* 每次找到有效的节点 都会把 其节点内存储的 thread 赋值给 s , 直到t指针遍历到 node 本身时
*/
if (t.waitStatus <= 0)
s = t;
}
// 找到了 正常的唤醒节点 , 就去唤醒该线程
if (s != null)
LockSupport.unpark(s.thread);
}