ReentrantLock核心源码分析,AQS独占模式,可重入锁
Posted 可持续化发展
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ReentrantLock核心源码分析,AQS独占模式,可重入锁相关的知识,希望对你有一定的参考价值。
看前须知
由于笔记比较多,看的时候,建议把代码copy到VS code 或者IDEA 中查看(为了可以点击方法进行跳转,方便阅读)。主要看方法上面的注释。本篇文章的图片来源于小刘老师的源码培训班。哔哩哔哩搜素:小刘讲源码。本篇文章仅用于作者本人复习源码知识点。
我的笔记
public class ReentrantLock implements Lock, java.io.Serializable {
//默认使用非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//sync 在运行时要么是 FairSync 子类,要么是 NonfairSync 子类
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//ReentrantLock.lock时,会调用相应sync 子类的 lock方法
public void lock() {
sync.lock();//这个是不响应中断的lock
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);//响应中断的lock,这个lock 会用到cancelAcquire方法
}
public void unlock() {
sync.release(1);
}
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
/**
学习ReentrantLock 前,须知
本笔记中带有//......的地方是指在ReentrantLock 这个可重入的独占锁中没有用到的代码,就不贴出来了。
为了方便阅读,我把一些方法移动了位置,注意看方法的注释。它表明了这个方法的所属类。
ReentrantLock 公平锁模式的lock方法,这个方法是不响应中断的。这里所谓的不响应中断指的是
线程A被 park后,给它一个中断信号,它醒来后,如果A线程的节点不是head的第一个后继 或 锁还没有被释放,
A线程会在acquireQueued中经过自旋,又会被park。
公平锁加锁的调用链如下:
ReentrantLock.lock -> FairSync.lock ->AQS.acquire -> FairSync.tryAcquire
-> AQS.addWaiter -> AQS.acquireQueued -> AQS.shouldParkAfterFailedAcquire
-> AQS.parkAndCheckInterrupt -> 返回AQS.acquire方法中调用selfInterrupt,再次给当前线程一个中断信号
-> 最后返回 业务层逻辑了。
先讲解的是FairSync。
AQS的等待者队列(head.next -> tail)【双向队列的节点看 waitStatus、thread】、 exclusiveOwnerThread、AQS.state
先对在ReentrantLock的公平锁中的AQS等待者队列的基本情况进行介绍:
这个队列是FIFO的、延迟初始化的双向队列。为什么要双向?因为它有那种操作需求:获取当前节点的前置节点,需要判断当前节点是不是head节点的第一个后继节点。
因为当前节点如果是head.next节点,才有权利去争夺锁tryAcquire.
延迟初始化是指第一个线程来的时候,直接获取锁成功,将AQS.state从0改为1,exclusiveOwnerThread设为当前线程。
这时,没有生成队列的head节点。第二个线程来的时候,在AQS.enq()方法中会为第一个线程补上一个Node节点作为head。而这个Node对象是空的构造方法。
head的意义是逻辑上代表当前独占锁的线程。head节点的thread 为null。waitStatus为-1.
队列的入队(关注Node节点的waitStatus、thread、prev、next)
head节点延迟加载。
在入队的过程中,当前时刻最后插入的node节点a的waitStatus等于0。
过一会再插入一个节点b的话,b会将a的waitStatus由0改为Node.SIGNAL(-1)。
反正最后一个入队的节点.waitStatus等于0
队列的出队,FIFO。新的head的thread设为null。
取消排队
AQS中有个cancelAcquire方法,取消排队的。看下文。
唤醒的情况有2种:
1.中断方式的唤醒,看看lock是否响应中断。
如果响应中断,就向上抛出异常,调用cancelAcquire方法取消排队。
如果不响应中断,有可能继续被park。
2.调用ReentrantLock.unlock的方式去唤醒
*/
static final class FairSync extends Sync {
//公平锁入口..
//不响应中断的加锁..响应中断的 方法为:lockinterrupted。。
final void lock() {
acquire(1);
}
/**
* AQS.acquire
* 这个方法就是为了获取锁。如果获取失败就会入队,park。醒来后再尝试获取。最后看看要不要设置中断
* 它会调用FairSync.tryAcquire、AQS.addWaiter、AQS.acquireQueued、AQS.selfInterrupt方法。
* 我们一个一个分析。
*/
public final void acquire(int arg) {
//条件一:!tryAcquire 尝试获取锁 获取成功返回true 获取失败 返回false。
//条件二:2.1:addWaiter 将当前线程封装成node入队
// 2.2:acquireQueued 挂起当前线程 唤醒后相关的逻辑..
// acquireQueued 返回true 表示挂起过程中线程被中断唤醒过.. false 表示未被中断过..
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//再次设置中断标记位 true
//lock方法完了以后,回到业务层面了。如果业务层面的代码是响应中断的话,就会去处理一些中断逻辑
//如果不响应中断的话,就什么也不会发生。
selfInterrupt();
}
/**
* tryAcquire方法的返回值意义是
* 尝试获取锁
* 抢占成功:返回true 包含重入..
* 抢占失败:返回false
*
* 大体逻辑是:
* 1.获取AQS的state
* 2.如果state等于0,说明当前AQS处于无锁状态,就会去检查
* 队列中除了head节点,在当前线程前面是否有等待者线程。
* 2.1.如果没有等待者线程,当前线程就会通过CAS的方式将AQS.state从0改为1,
* 并将ExclusiveOwnerThread设为当前线程,return true;
*
* 2.2.如果当前线程前面有等待者线程,就返回false,获取锁失败,要入队,被挂起。
* 3.如果AQS.state不等于0,就会再去判断 当前线程是不是独占锁线程。如果是的话,就会走重入锁的逻辑。return true;
* 4.如果尝试获取锁失败了。return false;
* 可能原因:
* (1)CAS竞争失败了。
* (2)当前线程不是独占锁线程,独占锁线程还没有释放锁。AQS.state大于0
*/
protected final boolean tryAcquire(int acquires) {
//current 当前线程
final Thread current = Thread.currentThread();
//AQS state 值
int c = getState();
//条件成立:c == 0 表示当前AQS处于无锁状态..
if (c == 0) {
//条件一:
//因为fairSync是公平锁,任何时候都需要检查一下 队列中在当前线程之前是否有等待者..
//hasQueuedPredecessors() 方法返回 true 表示当前线程前面有等待者,当前线程需要入队等待
//hasQueuedPredecessors() 方法返回 false 表示当前线程前面无等待者,直接尝试获取锁..
//条件二:compareAndSetState(0, acquires)
//成功:说明当前线程抢占锁成功
//失败:说明存在竞争,且当前线程竞争失败..
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//成功之后需要做什么?
//设置当前线程为 独占者 线程。
setExclusiveOwnerThread(current);
return true;
}
}
//执行到这里,有几种情况?
//c != 0 大于0 的情况,这种情况就需要检查一下 当前线程是不是 独占锁的线程,因为ReentrantLock是可以重入的.
//条件成立:说明当前线程就是独占锁线程..
else if (current == getExclusiveOwnerThread()) {
//锁重入的逻辑..
//nextc 更新值..
int nextc = c + acquires;
//越界判断,当重入的深度很深时,会导致 nextc < 0 ,int值达到最大之后 再 + 1 ...变负数..
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//更新的操作
setState(nextc);
return true;
}
//执行到这里?
//1.CAS失败 c == 0 时,CAS修改 state 时 未抢过其他线程...
//2.c > 0 且 ownerThread != currentThread.
return false;
}
/**AQS.hasQueuedPredecessors */
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
/**
* AQS.addWaiter里面有入队逻辑
* 最终返回一个封装了当前线程的node,这个node此时是已经入队了的。
* addWaiter的大体逻辑:
* 1.利用当前线程封装一个node对象。
* 2.如果队列中已经有节点了,则快速入队,
* 2.1.当前节点prev指向tail;
* 2.2.通过CAS的方式去移动tail引用,tail指向当前节点
* 2.3.CAS成功后,将老的tail.next指向当前节点。return 封装了当前线程的node对象。
* 3.如果当前队列为空 或者 CAS竞争入队失败了,就调用AQS.enq方法,进行完整入队操作。
* AQS.enq方法会保证封装了当前线程的node节点一定入队。
*/
private Node addWaiter(Node mode) {
//Node.EXCLUSIVE
//构建Node ,把当前线程封装到对象node中了
Node node = new Node(Thread.currentThread(), mode);
//快速入队
//获取队尾节点 保存到pred变量中
Node pred = tail;
//条件成立:队列中已经有node了
if (pred != null) {
//当前节点的prev 指向 pred
node.prev = pred;
//cas成功,说明node入队成功
if (compareAndSetTail(pred, node)) {
//前置节点指向当前node,完成 双向绑定。
pred.next = node;
return node;
}
}
//什么时候会执行到这里呢?
//1.当前队列是空队列 tail == null
//2.CAS竞争入队失败..会来到这里..
//完整入队..
enq(node);
return node;
}
/**
* AQS.enq()
* 返回值:返回当前节点的 前置节点。
* enq方法保证当前线程封装的node 一定会加入到队列里面
* 大体逻辑:
* 1.如果当前队列是空队列,作为当前持锁线程的 第一个后继线程,需要为当前持锁线程补一个node节点作为head。
* 因为这个队列是延迟初始化的。补完head节点后,就自旋入队
* (当前节点prev指向tail,用CAS来移动tail,tail指向当前节点,老的tail.next指向当前节点)。
* 如果CAS成功,就说明入队成功,构建好双向连接后,就return 当前节点的 前置节点。如果CAS竞争失败,就继续自旋。
* 2.如果当前队列不是空队列,就通过CAS的方式,尝试入队。CAS成功,就入队了。失败,则继续自旋,去尝试入队。
* 一定会入队成功。
*/
private Node enq(final Node node) {
//自旋入队,只有当前node入队成功后,才会跳出循环。
for (;;) {
Node t = tail;
//1.当前队列是空队列 tail == null
//说明当前 锁被占用,且当前线程 有可能是第一个获取锁失败的线程(当前时刻可能存在一批获取锁失败的线程...)
if (t == null) { // Must initialize
//作为当前持锁线程的 第一个 后继线程,需要做什么事?
//1.因为当前持锁的线程,它获取锁时,直接tryAcquire成功了,没有向 阻塞队列 中添加任何node,所以作为后继需要为它擦屁股..
//2.为自己追加node,在自己前面补一个node节点作为head
//CAS成功,说明当前线程 成为head.next节点。
//线程需要为当前持锁的线程 创建head。
if (compareAndSetHead(new Node()))
tail = head;
//注意:这里没有return,会继续for。。
}else {
//普通入队方式,只不过在for中,会保证一定入队成功!
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* AQS.acquireQueued方法需要 park当前节点的线程 和 唤醒后的逻辑。竞争资源
* 返回值的意义:
* true -> 表示当前线程在挂起的时候,被中断唤醒过
* false ->表示当前线程在挂起的时候,未被中断过
* 形参 node -> 已经入队的封装了当前线程的node对象
* 形参 arg -> 这个arg一般是1 。是当前线程尝试获取锁时,通过CAS的方式修改state时用的,从0改为1。
* 大体逻辑是
* 1.开一个for自旋,获取当前节点的前置节点。
* 2.如果 (前置节点是head && tryAcquire尝试获取锁成功了),就做老head的出队操作
* 2.1.将自己设为head。node.thread设为null,node.prev设为null。
* 2.2.把老的head.next设为null,帮他 GC
* 2.3.返回 interrupted。是否被中断过的布尔标记。
* 3.如果 当前节点不是head.next节点 || tryAcquire尝试获取锁失败了,就 (head.next被唤醒后才有资格去抢锁tryAcquire)
* 3.1.调用 shouldParkAfterFailedAcquire方法,判断一下是否需要park 当前线程。
* shouldParkAfterFailedAcquire方法要么true,要么false
* true -> 表示前置节点的waitStatus为SIGNAL(-1),当前线程需要被挂起。
* false -> 有2种情况:
* 情况1:前置节点的waitStatus大于0,前置节点处于取消状态。
* 这个方法会将处于取消状态的node节点出队,给当前节点找一个 waitStatus小于等于0 的前置节点pred。构建双向连接
* node.prev = pred; pred.next = node;
* 情况2:前置节点的waitStatus等于0。就是插入队尾的情况。
* 这个方法会将前置节点的waitStatus设为-1。表示前置节点释放锁之后需要 喊醒我
* 3.2.如果shouldParkAfterFailedAcquire返回true,就调用LockSupport.park(this); 挂起当前线程
* 3.3.如果shouldParkAfterFailedAcquire返回false,就自旋。
* 当前线程被挂起后,
* 如果unpark 唤醒的,就会做自旋操作。
* 如果是被其他线程唤醒的,就会将interrupted设为true,再做自旋操作。但公平锁的lock方法是不响应中断的,自旋后,大概率会被再次挂起。
* 公平锁的lockInterruptibly方法会响应中断。线程被挂起后,中断唤醒,会抛出异常,调用cancelAcquire方法做出队操作。
*/
final boolean acquireQueued(final Node node, int arg) {
//true 表示当前线程抢占锁成功,普通情况下【lock】 当前线程早晚会拿到锁..
//false 表示失败,需要执行出队的逻辑... (回头讲 响应中断的lock方法时再讲。)
boolean failed = true;
try {
//当前线程是否被中断过
boolean interrupted = false;
//自旋..
for (;;) {
//什么时候会执行这里?
//1.进入for循环时 在线程尚未park前会执行
//2.线程park之后 被唤醒后,也会执行这里...
//获取当前节点的前置节点..
final Node p = node.predecessor();
//条件一成立:p == head 说明当前节点为head.next节点,head.next节点在任何时候 都有权利去争夺锁.
//条件二:tryAcquire(arg)
//成立:说明head对应的线程 已经释放锁了,head.next节点对应的线程,正好获取到锁了..
//不成立:说明head对应的线程 还没释放锁呢...head.next仍然需要被park。。
if (p == head && tryAcquire(arg)) {
//拿到锁之后需要做什么?
//设置自己为head节点。
setHead(node);
//将上个线程对应的node的next引用置为null。协助老的head出队..
p.next = null; // help GC
//当前线程 获取锁 过程中..没有异常
failed = false;
//返回当前线程的中断标记..
return interrupted;
}
//shouldParkAfterFailedAcquire 这个方法是干嘛的? 当前线程获取锁资源失败后,是否需要挂起呢?
//返回值:true -> 当前线程需要 挂起 false -> 不需要..
//parkAndCheckInterrupt() 这个方法什么作用? 挂起当前线程,并且唤醒之后 返回 当前线程的 中断标记
// (唤醒:1.正常唤醒 其它线程 unpark 2.其它线程给当前挂起的线程 一个中断信号..)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//interrupted == true 表示当前node对应的线程是被 中断信号唤醒的...
interrupted = true;
}
}finally {//lock()方法调用时,这块逻辑基本用不到。
if (failed)
cancelAcquire(node);
}
}
}
/**
* 到这里为止,ReentrantLock的公平锁的加锁过程就完了。
* 下面说 公平锁的解锁过程。
* 公平锁解锁的调用链是:
* ReentrantLock.unlock -> AQS.release ->
* sync.tryRelease -> AQS.unparkSuccessor -> AQS.acquireQueued
*
* release的大体逻辑为
* 1.调用tryRelease方法去释放锁,如果当前线程完全释放锁了,就return true。如果未完全释放锁,则return false。因为有重入锁的逻辑。
* 如果未完全释放锁,这个方法就直接返回了。
* 2.如果当前线程完全释放锁了,则会调用unparkSuccessor方法去做唤醒操作。
* 如果当前节点是最后一个节点了(waitStatus=0),则不用做唤醒操作,直接返回true。
*/
public final boolean release(int arg) {
//尝试释放锁,tryRelease 返回true 表示当前线程已经完全释放锁
//返回false,说明当前线程尚未完全释放锁..
if (tryRelease(arg)) {
//head什么情况下会被创建出来?
//当持锁线程未释放线程时,且持锁期间 有其它线程想要获取锁时,其它线程发现获取不了锁,而且队列是空队列,此时后续线程会为当前持锁中的
//线程 构建出来一个head节点,然后后续线程 会追加到 head 节点后面。
Node h = head;
//条件一:成立,说明队列中的head节点已经初始化过了,ReentrantLock 在使用期间 发生过 多线程竞争了...
//条件二:条件成立,说明当前head后面一定插入过node节点。
//h.waitStatus != 0这一步主要是为了 最后一个node节点释放锁的逻辑,
// 最后一个节点 head、tail都指向它,waitstatus=0,就不用再唤醒后面的节点了。因为后面没有节点嘛
if (h != null && h.waitStatus != 0)
//唤醒后继节点..
unparkSuccessor(h);
return true;
}
return false;
}
/**
* Sync.tryRelease() 尝试去释放锁
* tryRelease的大体逻辑:
* 1.将当前AQS.state 减1
* 2.如果state等于0,说明当前线程完全释放锁了。将exclusiveOwnerThread设为null。返回 true
* 3.如果state不等于0,说明当前线程未完全释放锁。返回 false
*/
protected final boolean tryRelease(int releases) {
//减去释放的值..
int c = getState() - releases;
//条件成立:说明当前线程并未持锁..直接异常.,.
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//当前线程持有锁..
//是否已经完全释放锁..默认false
boolean free = false;
//条件成立:说明当前线程已经达到完全释放锁的条件。 c == 0
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//更新AQS.state值
setState(c);
return free;
}
/**
* AQS.unparkSuccessor 要唤醒后面的等待者线程。调用时机:释放锁的时候、取消排队的时候
* unparkSuccessor的大体逻辑为:
* 1.获取当前节点的waitStatus,获取当前节点.next节点,就是它的直接后继s。
* 2.如果后继节点s 为空 || s.waitStatus 大于0(取消状态),则
* 从后往前找,找一个位于当前节点后面的距离最近的 waitStatus小于等于0 的节点,将其封装的线程唤醒。
* 如果找到了,就唤醒节点封装的线程 LockSupport.unpark对应的线程
* 如果没找到,就返回了。
* 3.如果后继节点s 不为空,则唤醒其封装的thread。LockSupport.unpark
* 被唤醒的线程会从AQS.acquireQueued方法的parkAndCheckInterrupt处开始往下执行,自旋。
* ......就是acquireQueued的逻辑了。
*/
private void unparkSuccessor(Node node) {
//获取当前节点的状态
int ws = node.waitStatus;
if (ws < 0)//-1 Signal 改成零的原因:因为当前节点即将完成唤醒后继节点的任务了..
compareAndSetWaitStatus(node, ws, 0);
//s是当前节点 的第一个后继节点。
Node s = node.next;
//条件一:
//s 什么时候等于null?
//1.当前节点就是tail节点时 s == null。
//2.当新节点入队未完成时(1.设置新节点的prev 指向pred 2.cas设置新节点为tail(已经算入队了) 3.(未完成)pred.next -> 新节点 )
//需要找到可以被唤醒的节点..
//条件二:s.waitStatus > 0 前提:s != null
//成立:说明 当前node节点的后继节点是 取消状态... 需要找一个合适的可以被唤醒的节点..
if (s == null || s.waitStatus > 0) {
//查找可以被唤醒的节点...
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
//上面循环,会找到一个离当前node最近的一个可以被唤醒的node。 node 可能找不到 node 有可能是null、、
}
//如果找到合适的可以被唤醒的node,则唤醒.. 找不到 啥也不做。
if (s != null)
LockSupport.unpark(s.thread);
}
/**
* 到这里为止,公平锁解锁的逻辑讲完了。下面说一下,ReentrantLock的公平锁中响应中断的lock
* lockInterruptibly 响应中断的加锁逻辑,就是可以调用 AQS.cancelAcquire方法的。
*
* 不要忘记sync的运行时状态是什么类。sync = fair ? new FairSync() : new NonfairSync();
*
* 调用链是:
* ReentrantLock.lockInterruptibly -> AQS.acquireInterruptibly -> FairSync.tryAcquire
* -> AQS.doAcquireInterruptibly -> AQS.addWaiter
* -> shouldParkAfterFailedAcquire -> parkAndCheckInterrupt
* 前面和普通的公平锁 lock 差不多,区别在于:
* 当前线程被中断唤醒后,会向上抛出一个异常,throw new InterruptedException();
* 然后,走到finally块中,执行 cancelAcquire ,做取消排队的操作。
* 响应中断的lock,才会做取消排队的操作。
*
*/
/**
* AQS.cancelAcquire方法用于
* 取消指定node参与竞争。取消排队,出队。
* cancelAcquire方法的大体逻辑:形参为要取消排队的node节点
* 1.将node.thread设为null, node.waitStatus设为 1,cancelled状态
* 2.找一个最近的 waitStatus小于等于0 的前驱节点 pred
* 3.根据当前取消排队的node的位置不同,分情况:
* 3.1.当前node是队尾 tail -> node
* 通过CAS的方式将tail指向pred 节点,通过CAS的方式将pred.next改为null
* 3.2.当前node 不是 head.next 节点,也不是 tail
* 3.2.1.pred.waitStatus 等于-1,|| 等于0,等于0的时候,会用CAS再设为-1,保证pred.waitStatus等于-1后,
* 就出队:pred.next -> node.next 节点后,当node.next节点 被唤醒后,
* 调用 shouldParkAfterFailedAcquire 会让node.next 节点越过取消状态的节点
* 完成真正出队操作。
* 3.2.2.pred.waitStatus 等于 1,极端情况。
* 这种情况的话,会调用unparkSuccessor方法去唤醒当前节点的后继节点,后继节点
* 会调用shouldParkAfterFailedAcquire方法让取消状态的节点出队
* 3.3.当前node 是 head.next节点。
* 当前节点会调用unparkSuccessor方法唤醒后继节点。
* 后继节点唤醒后,会调用 shouldParkAfterFailedAcquire方法,
* 让node.next 节点越过取消状态的节点,并与 head 构建双向连接。
* 4.最后 node.next = node; // help GC 让这个节点的next 指向自己
*/
private void cancelAcquire(Node node) {
//空判断..
if (node == null)
return;
//因为已经取消排队了..所以node内部关联的当前线程,置为Null就好了。。
node.thread = null;
//获取当前取消排队node的前驱。
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//拿到前驱的后继节点。
//1.当前node
//2.可能也是 ws > 0 的节点。
Node predNext = pred.next;
//将当前node状态设置为 取消状态 1
node.waitStatus = Node.CANCELLED;
/**
* 当前取消排队的node所在 队列的位置不同,执行的出队策略是不一样的,一共分为三种情况:
* 1.当前node是队尾 tail -> node
* 2.当前node 不是 head.next 节点,也不是 tail
* 3.当前node 是 head.next节点。
*/
//条件一:node == tail 成立:当前node是队尾 tail -> node
//条件二:compareAndSetTail(node, pred) 成功的话,说明修改tail完成。
if (node == tail && compareAndSetTail(node, pred)) {
//修改pred.next -> null. 完成node出队。
compareAndSetNext(pred, predNext, null);
} else {
//保存节点 状态..
int ws;
//第二种情况:当前node 不是 head.next 节点,也不是 tail
//条件一:pred != head 成立, 说明当前node 不是 head.next 节点,也不是 tail
//条件二: ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
//条件2.1:(ws = pred.waitStatus) == Node.SIGNAL 成立:说明node的前驱状态是 Signal 状态 不成立:前驱状态可能是0 ,
// 极端情况下:前驱也取消排队了..
//条件2.2:(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))
// 假设前驱状态是 <= 0 则设置前驱状态为 Signal状态..表示要唤醒后继节点。
//if里面做的事情,就是让pred.next -> node.next ,所以需要保证pred节点状态为 Signal状态。
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//情况2:当前node 不是 head.next 节点,也不是 tail
//出队:pred.next -> node.next 节点后,当node.next节点 被唤醒后
//调用 shouldParkAfterFailedAcquire 会让node.next 节点越过取消状态的节点
//完成真正出队。
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//当前node 是 head.next节点。 更迷了...
//类似情况2,后继节点唤醒后,会调用 shouldParkAfterFailedAcquire 会让node.next 节点越过取消状态的节点
//队列的第三个节点 会 直接 与 head 建立 双重指向的关系:
//head.next -> 第三个node 中间就是被出队的head.next 第三个node.prev -> head
unparkSuccessor(node);
}
node.next = node; // help GC 让这个节点的指针指向自己
}
}
/**
* ReentrantLock的非公平锁 NonfairSync,
* 非公平锁也有响应中断的lock(lock)和不响应中断的lock(lockInterruptibly)
* 公平锁和非公平锁比较类似,它们的区别在于:
* 非公平锁lock的时候,会直接尝试用CAS 的方式 将AQS.state 从0改为1,去抢锁。没有管阻塞队列中是否有等待者线程。
* 如果拿到锁了,就直接将exclusiveOwnerThread设为当前线程。
* 如果拿不到,会调用tryAcquire方法。
* tryAcquire方法在state等于0的时候,也尝试用CAS的方式去抢锁(没有管阻塞队列中是否有等待者线程),还会尝试重入锁逻辑。
* 如果还是拿不到,就入队,挂起,等待被唤醒。
* 非公平锁的吞吐量比公平锁要稍微好一些。
* 公平锁在加锁的时候会判断当前队列中是否有其他线程在等待。非公平锁不会判断。
*
*/
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
static final class Node {
//枚举:独占模式
static final Node EXCLUSIVE = null;
//表示当前节点处于 取消 状态
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
//注释:表示当前节点需要唤醒他的后继节点。(SIGNAL 表示其实是 后继节点的状态,需要当前节点去喊它...)
static final int SIGNAL = -1;
//node状态,可选值(0 , SIGNAl(-1), CANCELLED(1), CONDITION, PROPAGATE)
// waitStatus == 0 默认状态
// waitStatus > 0 取消状态(在ReentrantLock模式下)
// waitStatus == -1 表示当前node如果是head节点时,释放锁之后,需要唤醒它的后继节点。
volatile int waitStatus;
//因为node需要构建成 fifo 队列, 所以 prev 指向 前继节点
volatile Node prev;
//因为node需要构建成 fifo 队列, 所以 next 指向 后继节点
volatile Node next;
//当前node封装的 线程本尊..
volatile Thread thread;
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//......
}
//头结点 任何时刻 头结点对应的线程都是当前持锁线程。这个线程是活跃状态的
private transient volatile Node head;
//阻塞队列的尾节点 (阻塞队列不包含 头结点 head.next ---> tail 认为是阻塞队列)
private transient volatile Node tail;
//表示资源
//独占模式:0 表示未加锁状态 >0 表示已经加锁状态
private volatile int state;
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
/**
* 就是给当前节点找一个好爸爸,给爸爸设为 -1
* 总结:
* 1.当前节点的前置节点是 取消状态 ,第一次来到这个方法时 会越过 取消状态的节点, 第二次 会返回true 然后park当前线程
* 2.当前节点的前置节点状态是0,当前线程会设置前置节点的状态为 -1 ,第二次自旋来到这个方法时 会返回true 然后park当前线程.
*
* 参数一:pred 当前线程node的前置节点
* 参数二:node 当前线程对应node
* 返回值:boolean true 表示当前线程需要挂起..
*/
//AQS#
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前置节点的状态
//waitStatus:0 默认状态 new Node() ; -1 Signal状态,表示当前节点释放锁之后会唤醒它的第一个后继节点; >0 表示当前节点是CANCELED状态
int ws = pred.waitStatus;
//条件成立:表示前置节点是个可以唤醒当前节点的节点,所以返回true ==> parkAndCheckInterrupt() park当前线程了..
//普通情况下,第一次来到shouldPark。。。 ws 不会是 -1
if (ws == Node.SIGNAL)
return true;
//条件成立: >0 表示前置节点是CANCELED状态
if (ws > 0) {
//找爸爸的过程,条件是什么呢? 前置节点的 waitStatus <= 0 的情况。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//找到好爸爸后,退出循环
//隐含着一种操作,CANCELED状态的节点会被出队。
pred.next = node;
} else {
//当前node前置节点的状态就是 0 的这一种情况。
//将当前线程node的前置node,状态强制设置为 SIGNAl,表示前置节点释放锁之后需要 喊醒我..
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//AQS#parkAndCheckInterrupt
//park当前线程 将当前线程 挂起,唤醒后返回当前线程 是否为 中断信号 唤醒。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//......
}
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
//独占模式下表示当前持有锁的线程
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
//......
}
图片
以上是关于ReentrantLock核心源码分析,AQS独占模式,可重入锁的主要内容,如果未能解决你的问题,请参考以下文章