一、前言
本篇是看了下面大神的专栏用来总结记录,因为自己写一遍总比看一遍记得清楚,但强烈建议想要详细了解AQS还是去阅读下面大神的专栏,而非我这篇文章。https://segmentfault.com/a/1190000015739343
二、介绍
1. AQS 简介
AQS是AbstractQueuedSynchronizer
的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。
AQS提供独占和共享两种模式,独占顾名思义,同一时间只能有一个线程占有锁,共享则是同一时间可以有多个线程拥有锁。
2. Node 数组
AbstractQueuedSynchronizer
中,队列的实现是一个双向链表,他的每个节点是一个Node类型
Node是 AbstractQueuedSynchronizer
的一个内部类,下面省略了部分代码:
static final class Node {
// 共享锁和独占锁的判断标志
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
// waitStatus 可选值
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 节点的等待状态,这个后面详细介绍
volatile int waitStatus;
// 当前节点的前置节点
volatile Node prev;
// 当前节点的后置节点
volatile Node next;
// Node 数组中所代表的线程
volatile Thread thread;
// 标志位,如果是null则说明是独占锁,不为null说明是共享锁
Node nextWaiter;
// 判断是否是共享锁
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前置节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
....
}
(1) waitStatus : 表示节点所处的等待状态
上面只有四种状态,实际上是有五种,
值 | 解释 |
---|---|
CANCELLED | 值为1,代表当前Node取消排队等待 |
SIGNAL | 值为-1,这个状态代表的并不是本身,而是代表当当前节点取消或者获取到锁时,需要唤醒后继的节点 |
CONDITION | 值为-2,在条件队列中才有用,代表线程处于正常的等待状态 |
PROPAGATE | 值为-3, 共享锁中使用,应该被无条件的传播到其他节点 |
0 | 节点初始化时waitStatus 的初始值 |
(2) prev、next 前后节点
因为AQS中的队列的实现是一个双向链表,所以需要连接前后节点,prev指向前置节点,next指向后置节点。
(3) thread
Node节点保存的线程信息
(4) nextWaiter
标志当前模式是共享还是独占。如果为null,说明是独占模式,如果为一个Node节点(这个节点不代表任何线程信息,仅仅用来标识),则说明是共享模式。
三、独占锁的实现
从ReentrantLock类中看
ReentrantLock#lock -> ReentrantLock.FairSync#lock -> AbstractQueuedSynchronizer#acquire
1. 独占锁的获取
(1). AbstractQueuedSynchronizer#acquire
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
从上面的代码可以看到,如果tryAcquire(arg)
尝试获取锁失败后才会执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
的代码。我们先来看tryAcquire(arg)
方法。 tryAcquire
方法在 AbstractQueuedSynchronizer
并没有具体的实现,因为尝试获取锁的逻辑一般是根据子类的需求来的,所以这个方法在子类中有具体的实现,如下:
(2) ReentrantLock.FairSync#tryAcquire
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取标记量,如果等于0则说明还没有线程获取锁
int c = getState();
if (c == 0) {
// 如果等待队列中没有在当前线程前面的等待线程,则使用CAS将state置为acquires,
// 并且记录下来获取锁的线程(因为是独占锁)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果state不等于0,则说明有线程获得了锁,判断是否是当前线程获取的锁,如果是,则累加state(因为是可重入锁)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
(3).AbstractQueuedSynchronizer#addWaiter
顺着上面的逻辑走下来。 如果 tryAcquire
尝试获取锁失败后,会调用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法。
addWaiter()
方法是将当前获取锁失败的线程放入等待队列中。Node.EXCLUSIVE
代表的是独占模式,其实就是null。而共享模式则是一个没有其他意义的Node节点。
而在Node的构造函数中可以看到 Node 节点 使用 nextWaiter
作为一个标识,为null则代表是独占模式,一个Node节点则代表共享模式,但是这个Node节点不代表任何线程,仅仅做一个标识意义。
下面具体分析 addWaiter
方法
/**
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 将当前线程包装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 如果队列不为空(因为tail是指向尾结点,如果他为空,则说明队列为空), 则将当前线程包装成Node节点入队末尾。
// 并且将tail 指向队尾节点。
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果队列为空,或者队尾元素已经变化(compareAndSetTail(pred, node) cas 操作失败),则会调用enq
enq(node);
return node;
}
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果队列为空,则创建一个头结点,这个头结点是新new出来的,所以不包含任何数据。
// 外层是个循环,跳出循环的唯一办法就是走else支路
if (t == null) { // Must initialize
// 创建头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 走到这里说明队列已经不为空,至少有了头结点。
// 让node前置节点指向 tail所指向的节点, 之后并设置tail指向node节点.(这里会造成尾分叉)
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter
的 完整的逻辑(我们假设现在Node队列是空,来走一遍逻辑):
1. 线程a进入addWaiter
方法,将线程a封装成一个Node节点 an。
2. 判断pred是否为null,这里因为Node队列为空,所以pred 必定为null,所以走向end(node)
;
3. 进入enq后,第一次循环,tail必然是null,则t为null,进入if 里面调用 compareAndSetHead(new Node())
初始化了一个空的头结点(不包含任何线程信息的头结点)并且让 tail指向 head。随后进入第二次循环
4. 第二次循环t不为null,所以走else。让 an.prev 指向 t,即指向了head。然后CAS设置尾指针指向an,并返回节点。
5. 此时线程b进入addWaiter
方法,将线程a封装成一个Node节点 bn。这时pred不为空,所以走进if里面。if中做的事情就是将bn入到队列尾,并使tail指向bn。
流程图如下:
注意:
因为 node.prev = t; 和后面的 if 操作并不是原子操作,所以导致在并发情况,an执行完 node.prev = t 后,bn进来也执行了 node.prev = t 。这样就导致两个新节点的前置节点都是尾结点。如下图,所以在AQS中很多循环都是倒序循环,因为存在尾分叉的情况,一个节点要能入队,则它的prev属性一定是有值的,但是它的next属性可能暂时还没有值。
(4). AbstractQueuedSynchronizer#acquireQueued
addWaiter
返回当前线程封装成的一个Node节点, 并将该节点加入了等待队列中。
接下来看 acquireQueued
方法
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取node节点的前置节点
final Node p = node.predecessor();
// 如果前置节点是头结点,则说明当前节点已经是等待线程中最前面的了(因为头结点并不代表任何等待线程),调用tryAcquire()尝试获取锁。
if (p == head && tryAcquire(arg)) {
// 如果锁获取成功,则将node设置为头节点(清空了锁代表的线程信息,可以理解为变相的出队),并返回
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果node前面还有等待的节点,则判断是否需要将当前线程挂起。
// 设置好闹钟后(shouldParkAfterFailedAcquire 返回true), 调用parkAndCheckInterrupt() 挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
....
// 可以看到如果node获取到锁,那么它将成为头结点,但是他的信息也被清空,不代表任何线程信息。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
...
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果前置节点的状态为 SIGNAL, 则说明已经设置了唤醒状态(订好了闹钟),直接返回true。
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 如果 前置节点的 ws 大于0(其实也就是取消状态),则说明前置节点已经取消排队了,则跳过这些取消的节点,直接跳到未取消的节点
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 否则的话,将前置节点状态置为SIGNAL,即后面的节点需要前置节点唤醒
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
(5). 总结
假设两个线程 t1、t2 需要获取锁。
- 当t1获取锁时,由于之前没有线程获取锁,所以
tryAcquire(arg)
尝试获取锁成功,返回true。注意!tryAcquire(arg)
为false,直接返回,此时并没有创建Node队列,连头结点也没有创建。
- t1还未释放锁时,t2获取锁,这时候
tryAcquire(arg)
获取失败,返回false,所以会调用addWaiter
方法创建一个头结点的队列,并且将当前线程添加进去。
- 顺着上面,进去enq方法。第一遍循环如下。
创建一个如下的头结点,其中nextWaiter懒得画, 反正也不影响这里。
注意外围是一个for循环,跳出循环的唯一办法就是走进else,所以我们第二次循环走进else,else会创建一个t2的node节点,并追加到head后,并肩t2的节点返回。
- t2 继续进入
acquireQueued
方法。这时会p == head
返回true,但是tryAcquire(arg)
肯定失败,因为这是是独占锁,t1还未释放锁,t2必然获取不到。
6. 随后执行 shouldParkAfterFailedAcquire
方法,在这个方法中会改变head节点的waitStatus
状态(仅仅是当前这个例子的情况下,并非每一次改变的都是头结点)
如下:
7. 至此,线程t2加入到了Node等待队列。随后调用 parkAndCheckInterrupt
阻塞线程
2. 独占锁的释放
我们 也从 RentrantLock#unlock
开始分析
(1). RentrantLock#unlock ->AbstractQueuedSynchronizer#release
因为RentrantLock 是独占锁,所以在下面的方法中当 tryRelease 方法返回true(即锁释放成功后),才会唤醒下一个等待线程。
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
// 尝试释放锁,tryRelease 的实现也是在子类中
if (tryRelease(arg)) {
Node h = head;
// 释放锁成功后,开始唤醒后继节点的线程
// h != null 说明队列不为空,h.waitStatus !=0 说明需要唤醒后记节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这里来详细说明一下 if (h != null && h.waitStatus != 0)
的条件,h != null 必然是队列不为空,队列都为空了还有什么好唤醒的。
问题是第二个条件 h.waitStatus != 0
更新waitStatus的地方只有AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
中 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
将ws更新成 Node.SIGNAL
。
而由于h是head节点,即头结点,那么什么时候会将头结点的waitStatus
状态更新呢?可以看到下面的图
不执行 shouldParkAfterFailedAcquire
方法的条件是 p == head && tryAcquire(arg)
。当p为头结点时,如果tryAcquire(arg)
尝试获取锁失败,仍然会执行 shouldParkAfterFailedAcquire
方法,并且会将 head头结点的waitStatus置为 Node.SIGNAL
。换句话说,如果head的waitStatus == 0
时,则说明头结点后面没有正在等待的节点。
(2). ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是拥有锁的线程,则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果更新后的状态等于0,则算释放了锁(因为可能是可重入锁,所以一次释放不一定完全释放)
if (c == 0) {
free = true;
// 设置拥有锁的线程为null,即没有线程拥有锁
setExclusiveOwnerThread(null);
}
// 更新状态值为0
setState(c);
return free;
}
(3). AbstractQueuedSynchronizer#unparkSuccessor
如果一个线程被挂起了, 它的前驱节点的 waitStatus值必然是Node.SIGNAL
/**
* Wakes up node's successor, if one exists.
*
* @param node the 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.
*/
// 如果head 的 ws 小于0,将其状态更新成0。
int ws = node.waitStatus;
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.
*/
// 一般情况下,我们只需要唤醒即是自己的后继节点
// 但是无法保证后继节点是否已经取消了排队
//所以这里是为了找到离自己最近的后继有效节点
Node s = node.next;
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;
}
// 如果找到了节点,则唤醒节点
if (s != null)
LockSupport.unpark(s.thread);
}
(4)总结
- t1执行结束了,释放锁了。这时候队列的状态如下图。
- t1 调用
release
方法释放锁。
- 因为锁是t1占用,所以释放成功。我们继续看下面的条件,介绍如下:
- 我们满足上述条件,进入
unparkSuccessor
方法。,5. 这时候的队列状态如下,又回到了一开始的样子。
下面根据个人理解解释一下 为什么 unparkSuccessor 方法要将head的节点waitStatus置为0
我们假设,head.watisStatus
状态仍为SIGNAL
。那么当t2释放锁时,他会再去执行
AbstractQueuedSynchronizer#unparkSuccessor
来释放线程,虽然最终结果相同,但是造成了不必要消耗。这也是他为什么没有判断是否更状态成功,并且官方注释上写着失败也无妨,因为确实失败也无妨。。。。
四、共享锁的实现
关于共享锁,共享锁的很多代码和独占锁类似,所以下面的讲解并没有那么细致。
我们通过 Semaphore
来进行研究
1. 共享锁的获取
我们从Semaphore#acquire() -> AbstractQueuedSynchronizer#acquireSharedInterruptibly
来看
(1). AbstractQueuedSynchronizer#tryAcquireShared
tryAcquireShared
在子类中实现,Semaphore中有公平锁和非公平锁两种实现,我们挑公平锁的实现看一看
Semaphore.FairSync#tryAcquireShared
protected int tryAcquireShared(int acquires) {
for (;;) {
// 如果等待队列中还有节点在当前线程前面,则说明没轮到当前线程,返回-1
if (hasQueuedPredecessors())
return -1;
// 到达这里说明已经是等待队列中最前面的节点了
int available = getState();
int remaining = available - acquires;
// 因为是共享锁,可能有多个线程同时拥有,只要剩余量remaining 大于0,说明还有通路,所以还可以允许其他线程申请,小于0或者CAS失败(失败则说明有其它线程可能更新了剩余量),则说明通路不够,返回剩余量
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
(2). AbstractQueuedSynchronizer#doAcquireSharedInterruptibly
doAcquireSharedInterruptibly
里面的很多代码都在独占锁讲过了,所以这里简化讲解过程。setHeadAndPropagate
后面单独讲
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程封装成一个Node节点,传参是代表共享节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 获取前置节点
final Node p = node.predecessor();
// 如果前置节点是头结点,说明轮到自己等待获取锁
if (p == head) {
// 尝试获取锁 -》 这里返回的是个int值,这里也是和独占锁的一个不同之处
// 独占锁返回boolean类型,因为锁只能被一个线程占用,其它线程不能占有
// 共享锁则是多个线程可以占有,如果返回值r 大于0,则说明还有剩余通路,将当前节点设置为头结点,清空节点信息(变相出队)后返回。
int r = tryAcquireShared(arg);
if (r >= 0) {
// 将当前获取锁的节点作为头结点,并且清空其信息(我只是一具没有感情的节点尸体..)也就是说的变相出队。因为虽然这个节点还在队列中,但是他已经作为头结点使用,并且没有任何线程信息。
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 达到这里,则说明获取锁失败, 这里和独占锁相同,不再讲解,即将线程节点挂起等待。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
(3). AbstractQueuedSynchronizer#setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
// 这里都很类似,设置头节点
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 传播释放锁
doReleaseShared();
}
}
(4). 总结
- 假设等待队列如下,有头结点head-t1(标志是t1线程转换成的头结点)
- 这时候线程t2获取锁。我们假设 t2 线程现在执行
Semaphore#acquire() -> AbstractQueuedSynchronizer#acquireSharedInterruptibly
来获取一个锁。
3. 我们假设 t2 线程现在执行 doAcquireSharedInterruptibly
来再次获取一次锁,进入到这里其实已经获取失败了(在acquireSharedInterruptibly
中已经执行过一次tryAcquireShared
并且获取失败了),所以将其加入到等待队列。
4. 进入 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
中阻塞队列。 (shouldParkAfterFailedAcquire(p, node)
返回true即代表将前置节点的waitStatus设置为SIGNAL状态了。
这时候的队列状态应该为下图。
5. 按照上面的逻辑,我们假设还有线程t3、t4入队等待,队列如下图
2. 共享锁的释放
(1). AbstractQueuedSynchronizer#releaseShared
Semaphore#release() -> AbstractQueuedSynchronizer#releaseShared
可以看到如果 tryReleaseShared
释放锁成功,开始解除线程的阻塞
/**
* Releases in shared mode. Implemented by unblocking one or more
* threads if {@link #tryReleaseShared} returns true.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryReleaseShared} but is otherwise uninterpreted
* and can represent anything you like.
* @return the value returned from {@link #tryReleaseShared}
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
Semaphore.Sync#tryReleaseShared
比较简单,不做过多解释
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
(2).AbstractQueuedSynchronizer#doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head;
// 代表着队列中除了头结点应该还有一个实际等待节点,即至少两个节点
if (h != null && h != tail) {
int ws = h.waitStatus;
// 1
if (ws == Node.SIGNAL) {
// 这里通过CAS操作保证了unparkSuccessor(h)只被执行一次。因为CAS保证只有一个线程可以修改成功
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 释放线程
unparkSuccessor(h);
}
// 2. Node.PROPAGATE 标志应该无条件传播。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 3
if (h == head) // loop if head changed
break;
}
}
解释:这一段强烈建议看上面推荐的原文章,这一段写的估计只有我自己能看懂。。。。。
1. if (ws == Node.SIGNAL)
显而易见,如果ws状态为SINGNAL ,则需要唤醒后继节点
2. else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
。 前半段,ws == 0 说明这个节点是当前队列的最后一个节点成为了头结点,因为节点挂起时会让前置节点的waitStatus置为SINGNAL。后半段,当 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE) 返回true时,也就是compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
失败了,也就是说再进行这个操作的时候,有新节点入队。AbstractQueuedSynchronizer#doAcquireInterruptibly -> AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
方法中设置了前置节点(即头结点)的waitStatus的状态为SINGNAL。这时候 continue,继续循环唤醒节点()。
3. 这里为什么要判断 h == head ? 因为 doReleaseShared 方法在 setHeadAndPropagate和releaseShared中都调用doReleaseShared 方法。这就导致当前线程执行这个方法到结尾时,其它线程可能也调用了doReleaseShared 方法并且修改了头结点,当头结点不一致时,则继续进入循环修改。
(3). 总结
- 假设等待队列如下,有头结点head-t1(标志是t1线程转换成的头结点)
-
我们假设这时候 t1线程执行结束。会调用
Semaphore#release(int) -> AbstractQueuedSynchronizer#releaseShared -> AbstractQueuedSynchronizer#doReleaseShared
方法释放锁。
-
当t1进入
doReleaseShared
方法去释放后继节点。很显然,这里的head 即是head-t1。显然满足if (h != null && h != tail)
的条件 (if (h != null && h != tail)
标志着着队列中包含头结点的情况下至少有两个节点,也就至少有一个等待线程节点)。所以进入 if 中。并且通过compareAndSetWaitStatus(h, Node.SIGNAL, 0)
将head-t1
的waitStatus
值置为0。这里的CAS操作是为了确保多个线程来操作时,只有一个线程能够修改头结点的值,也就是能执行下面的unparkSuccessor(h);
语句释放线程, 这里释放了线程 t2。
-
当线程 t2 释放后,t2线程继续自己的for循环,
doAcquireShared
方法中的循环,尝试获取锁。假设t2获取锁成功,这时候进入setHeadAndPropagate
方法中 还会 执行doReleaseShared
方法,并且在获取成功后还会修改头结点。
这时候队列变成如下:
-
需要注意的是: 这时候有两个线程在执行
doReleaseShared
方法 一个是刚开始释放锁的线程t1,一个是刚抢到锁的t2。对t1来说,当他执行到if (h == head)
时,t2可能已经执行结束了setHeadAndPropagate
方法更换了头结点,也就是说现在的头结点可能已经变成了head-t2,所以这时候对于t1线程来说会再次进入下一次循环,进行下一次的释放锁的调用。这样就形成了一个调用风暴,加快了释放锁的过程。(这里的描述仅仅是说有这种情况,并不代表每一次释放锁都会造成这样的情况)
-
重复上述过程将 t3、t4 也释放掉后,队列变成如下
-
这个时候我们再看
doReleaseShared
方法时, else if 分支什么时候可以进入ws == 0
只有队尾节点成为了头结点时才会如此。我们假设一个情况,if (h != null && h != tail)
条件后(这个条件有保证了必须有两个节点)。我们上面的图明显不满足这个条件。 -
那么假设这时候有了线程 t5 获取锁,执行 doAcquireShared 方法,并且获取锁失败,那么会执行
shouldParkAfterFailedAcquire
方法。我们这里就卡一个时间点,t5线程将要执行shouldParkAfterFailedAcquire
方法但是还未执行的时候,因为shouldParkAfterFailedAcquire
方法中将前置节点的waitStatus置为 SIGNAL ,但这里还未执行,所以前置节点的waitStatus仍保持原状。
-
这时候的队列状态就为下图,这时候就满足了
doReleaseShared
方法中的if (h != null && h != tail)
条件和else if (ws == 0
条件。
-
再看后半段的条件
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
想要他成立,那么也就是说头结点head-t4
的waitStatus
不为0了, 那么就是t5线程已经在shouldParkAfterFailedAcquire
方法中将前置节点的waitStatus
置为SIGNAL
。这时候队列状态为。当达到这个情况时,head-t4
会执行continue;
然后再次开始循环唤醒线程。
五、总结:
本篇文章内容贼长,叙述及其混乱。因为AQS还是很厉害的,看的我都一懵一懵的,所以写也懵,内容中不免有些错误,欢迎指正。另一方面,强推大佬的AQS四部曲 https://segmentfault.com/a/1190000015739343
。本文可以算是四部曲的心得体会(话说还有一部曲的心得没写,篇幅太长,下回分解)
以上:内容部分参考网络
https://www.jianshu.com/p/da9d051dcc3d
https://segmentfault.com/a/1190000015739343
https://segmentfault.com/a/1190000016447307
https://segmentfault.com/a/1190000015752512
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正