文章很长,请耐心观看,有不同见解的地方欢迎指出,谢谢大伙
Node节点内容
static final class Node {
// 节点引用相关开始
// 用于表明节点是处于共享模式
static final Node SHARED = new Node();
// 用于表明节点是处于独占模式
static final Node EXCLUSIVE = null;
// 上一个节点
volatile Node prev;
// 后一个节点
volatile Node next;
// Condition模式使用,下一个等待条件的节点
Node nextWaiter;
// 节点引用相关结束
// 状态值开始
static final int CANCELLED = 1; // 表明节点对应的线程已被取消
static final int SIGNAL = -1; // 表明后继节点的线程需要被唤醒
static final int CONDITION = -2; // 表明线程正在等待某条件
static final int PROPAGATE = -3; // 表明后续的共享锁需要无条件传播
volatile int waitStatus; // 当前节点保存的线程对应的等待状态
// 状态值结束
volatile Thread thread; // 当前节点保存的线程
// 是否为共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 获取前置节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { //用于初始化头节点或者共享标识
}
Node(Thread thread, Node mode) { // 用于添加队列waiter,mode一般使用到的是SHARED或者EXCLUSIVE
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 用于添加条件队列对象, waitStatus一般是CONDITION
this.waitStatus = waitStatus;
this.thread = thread;
}
}
CLH队列结构
CLH队列获取资源流程描述
- 尝试获取资源(子类锁需要提供的自定义方法tryAcquire);
- 获取不到资源,则入队(自旋+CAS直到入队成功);
- 唤醒入队节点 (也是线程被阻塞的位置);
- 如果入队节点的上一个节点是头节点,则直接尝试获取锁,如果获取成功就执行,并将其设置为头节点;
- 如果尝试获取锁不成功或者上一个节点非头节点,则判断其是否应该park(额外的处理: 前一个节点为SIGNAL,应该直接park;前一个节点是CANCELLED节点,则调整节点为非CANCELLED节点的后继;前一个节点非CANCELLED,则将其处理成SIGNAL; ),而后继续自旋,直到成功获取资源(因为被park醒来后也是在这个旋转内,会重复执行);
- 在1~5过程内异常导致线程在没有获取到资源的同时也没被park,就需要取消该线程当次获取资源的资格,处理流程为将该节点 的thread置空、waitStatus=CANCELLED,同时将CANCELLED节点 移出队列;但如果出现调整后该节点的前驱节点是头节点,则需要尝试唤醒该节点的后继节点;
CLH队列进队流程
-
addWaiter() 是在线程尝试获取不到锁的情况下将自己入队,此时仅仅是入队而已;
-
当入队后,会再执行acquireQueued(),此时如果线程尝试获取不到资源,则会再此步骤被park,当被其他线程唤醒,就继续在自旋2里继续尝试获取资源,如果依旧获取不到,继续park;
-
如果中途线程出现其他问题,如中断异常等,则会走cancelAcquire(),此步骤节点状态会被设置为Node.CANCELLED,节点线程被置空,并且可能会对队列里的CANCELLED节点进行丢弃,也可能会对节点 线程进行唤醒(有多条支路,所以说是可能 操作);
CLH队列进队(获取资源)源码解析
1、AQS队列独占锁获取的模板
public final void acquire(int arg) {
/**
* 1、尝试获取资源失败
* 2、入队
* 3、(自旋)获取资源,获取不到被park,当被unpark后依旧获取不到资源继续park
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
/**
* 这里需要重新响应 中断的原因是:
* acquireQueued 内部会获取线程的中断信息进行保存并且返回标识是否被中断
* 的布尔值,同时在获取的时候会将线程的中断信息擦除,不然无法对其park;
* 那当整个获取锁的过程走完后,就需要根据之前的中断信息设置是否需要重新
* 响应中断;
*/
selfInterrupt();
}
2、tryAcquire(int arg)
// AQS资源尝试获取的定制方法, 由子类提供
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
3、addWaiter(Node mode)
// 按照参数给定的模式(共享或者独占)创建节点并入队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 尾插法+CAS设置尾部节点引用
Node pred = tail;
// 相当于队列不为空
if (pred != null) {
node.prev = pred;
// CAS设置尾部节点引用 Unsafe.compareAndSwap
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 队列为空或者节点创建后CAS入队失败就会执行到这里
enq(node);
return node;
}
4、enq(final Node node)
// 将节点插入到队列里
private Node enq(final Node node) {
// 自旋的原因是因为如果有多个线程操作的话
// CAS 失败,就可以重新处理,直到正确入队
for (;;) {
Node t = tail;
if (t == null) { // 初始化队列,创建一个空内容节点
if (compareAndSetHead(new Node()))
tail = head;
} else { // CAS将新节点设置为尾部并入队
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
5、AQS核心方法(非常重要)
// 已进队的节点去尝试获取资源
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 当且仅当该线程获取到了锁才为false
try {
boolean interrupted = false; // 当且仅当该线程被park才为true
/**
* 当前自旋的作用,当线程走到parkAndCheckInterrupt()会被阻塞,
* 下次被唤醒后继续往下执行,
* 走if (p == head && tryAcquire(arg)) 这条支路
* 如果为上一个节点为头节点并且能够拿到资源则作为头节点,同时线程运行
*/
for (;;) {
// 1、获取最新节点的上一个节点
final Node p = node.predecessor();
// 2、上一个节点为头部则直接尝试获取资源
// 成功的话将node设置为头部、节点线程设置为空
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
// 这里的interrupt实际返回的是Thread.interrupted()
return interrupted;
}
/**
* 3、获取资源失败或者当前节点的上一个节点并非头节点
* 如果这个节点的前一个节点是SIGNAL,
* 则直接Park并且在被唤醒之后返回线程是否被中断的标志,
* 如果前一个节点非SIGNAL,则继续循环直到获取到资源或者阻塞
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
/**
* 顺利的情况下try里面的节点肯定是会获取到资源的,
* 此时走到这一步failed=false
* 但线程出现中断异常或者其他形式的异常,failed=true,
* 则需要取消acquire,个人理解为修复节点状态
*/
if (failed)
cancelAcquire(node);
}
}
6、shouldParkAfterFailedAcquire(Node pred, Node node)
// 该方法会针对上一个节点和当前节点去校验,最终返回当前节点是否应该park的布尔值
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
/**
* 1、如果上一个节点的状态为SIGNAL
* 则node最终会被前一个节点唤醒,此时安心park
*/
if (ws == Node.SIGNAL)
return true;
// 往下的内容都暂时不被Park,因为不确定node能不能被唤醒
/**
* 2、如果上一个节点的状态为CANCELLED
* 则往前找到非取消的节点,并将新节点的prev指向第一个非CANCELED的节点
* 相当于丢弃中间这一大堆取消了的节点
*/
if (ws > 0) {
do {
// pred = pred.prev;
// node.prev = pred;
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/**
* 3、前一个节点的状态 == 0 || == SIGNAL || == CONDITION || == PROPAGATE
* 不论为上面四个状态值的哪个,都将其改为SIGNAL,
* 因为新节点入队后需要由前置节点唤醒
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
7、parkAndCheckInterrupt()
// 该方法用于阻塞线程、设置锁和归还锁,并返回线程中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
/**
* 这一步返回往往会被人忽略。但恰恰这句话个人觉得算是acquireQueued()的核心之一
* 因为上一步虽然被park了,但程序唤醒过来后,咱也无法直接明确的说它就是被unpark的,
* 毕竟也有可能是中断导致的唤醒,所以此时需要记录中断状态,同时也对其做个状态重置。
*
* 那假如获取了锁,线程继续跑,此时就有必要利用已保存的中断信息去重新响应中断。
*/
return Thread.interrupted();
}
8、LockSupport.park(Object blocker)
public static void park(Object blocker) {
// 1、给当前线程设置锁; 2、park当前线程; 3、当线程被唤醒就将锁释放掉;
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
9、cancelAcquire(Node node)
// 此方法用于将节点取消,移出获取资源队列,同时有补充唤醒CANCELLED节点的后续节点的作用
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
/**
* 1、将节点的thread置空,waitStatus设置为CANCELLED
* 找到第一个非cancelled的节点,将节点的prev指向第一个非CANCELLED的节点
* 这里可能会疑惑为什么thread和waitStatus的信息修改不需要CAS?
* 原因是此处current线程修改自己的节点信息,不存在其他线程争抢
*/
node.thread = null;
// 找到第一个非cancelled的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
// 用于后续CAS处理其他节点的后置节点
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// 走到此处node的thread为空,waitStatus为CANCELLED
/**
* 2、如果当前节点为tail,则将当前节点的prev设置为tail
* 因为此时当前节点已经为cancelled,移出队列
*/
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
/**
* 3、如果当前节点非tail,则需要处理两个事情
* 3-1、如果当前节点的prev节点非队列的首节点(执行节点)
* 则将当前节点的后续节点入队(移除CANCELLED节点)
* 3-2、如果当前节点的prev为执行节点
* 则需要尝试唤醒当前节点的后续节点
*/
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
// pred不是头节点、
// (pred的状态是SIGNAL或者成功将pred的状态设置为SIGNAL)
// pred的线程非空(也就是还没执行)
// 则需要将当前节点后面的节点做一个链接
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)
compareAndSetNext(pred, predNext, next);
} else {
// 1、pred是头节点;
// 2、pred无法最终设置为SIGNAL;
// 3、pred的线程已经被调度
// 这里说明pred可能是头节点、
// 并且线程被调度同时已经status为CANCELLED,
// 即节点线程被取消执行或者执行完了
// 所以需要对后继节点进行唤醒
unparkSuccessor(node);
}
// TODO 作用暂时还未完全理解
node.next = node; // help GC
}
}
注: unparkSuccessor(node)放到释放资源章节一起讨论
会调整队列节点的代码位置, 如丢弃已经取消的节点
- cancelAcquire(Node node)
- shouldParkAfterFailedAcquire(Node pred, Node node)
- enq(final Node node)
- addWaiter(Node mode)
CLH队列释放资源流程
CLH队列出队(释放资源)源码解析
1、release(int arg)
// 独占模式下的资源释放方法。如果tryRelease返回true,则唤醒一个或者多个线程。
// 可被用于Lock的unlock()方法
public final boolean release(int arg) {
if (tryRelease( arg)) {
Node h = head;
/**
* CANCELLED SIGNAL PROGATION CONDITION
*
* CANCELLED 为什么也要唤醒?
* 节点线程执行异常或者被中断等,同样需要释放当前资源, 唤醒后续线程获取处理机
*/
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
2、tryRelease(int arg)
// 独占锁需要重写的释放锁逻辑
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
3、unparkSuccessor(Node node)
// 用于唤醒后继节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
/**
* 1、将非CANCELLED的节点状态置为0
* node非中断状态, 为其他状态,
* 说明线程已经正常执行到释放资源的位置
*/
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
/**
* 2、唤醒后继节点或从尾往前找到的第一个非CANCELLED节点。
* 2-1、正常情况下 唤醒情况是发生在node的next节点;
* 2-2、但如果node的next为空或者已经被取消,
* 则需要从tail往前找到第一个非空并且不等于node的节点,
* 同时如果该节点的线程还没被调度则唤醒它;
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
// CONDITION SIGNAL PROPAGATE 0
if (t.waitStatus <= 0)
s = t;
}
本章完,下节更新条件队列的个人理解O(∩_∩)O