文章目录
前言
尊重原创:原文链接。
AQS(AbstractQueuedSynchronizer)是Java中众多锁以及并发工具的基础,其底层采用乐观锁,大量使用了CAS操作,并且在冲突时,采用自旋方式重试,以实现轻量级和高效地获取锁。
AQS虽然被定义为抽象类,但事实上,它并不包含任何抽象方法。这是因为AQS是被设计来支持多种用途的,如果定义抽象方法,则子类在继承时必须要覆写所有的抽象方法,这显然不合理。所以AQS将一些需要子类覆写的方法都设计成protect方法,将其默认实现为抛出UnsupportedOperationException
异常。如果子类使用到这些方法,但是没有重写,则会抛出异常;如果子类没有使用到这些方法,则不需要做任何操作。
AQS中实现了锁的获取框架,锁的实际获取逻辑交由子类去实现,就锁的获取操作而言,子类必须重写tryAcquire()
方法。
Java并发工具类的三板斧
在开始看AQS源码之前,我们先来了解一下Java并发工具的设计套路,我把它总结为三板斧:状态、队列、CAS
。
- 状态:一般是一个state属性,它基本时整个工具的核心,通常整个工具都是在设置和修改状态。很多方法的操作都依赖于当前状态是什么。由于状态时全局共享的,一般会被设置成volatile类型,以保证其修改的可见性。
- 队列:队列通常是一个等待的集合,大多数以链表的形式实现。队列采用的是悲观锁的思想,表示当前所等待的资源、状态或者条件短时间内可能无法满足。因此,它会将当前线程包装成某种类型的数据结构,扔到一个等待队列中,当一定条件满足后,再从等待队列中取出。
- CAS:CAS操作时最轻量的并发处理,通常我们对于状态的修改都会用到CAS操作,因为状态可能被多个线程同时修改,CAS操作保证了同一个时刻,只有一个线程能修改成功,从而保证了线程安全。
AQS核心
在AQS中,状态是由state属性来表示的,使用volatile修饰。
private volatile int state;
该属性的值表示了锁的状态
,state为0表示锁没有被占用,state > 0 表示当前已经有线程持有该锁,这里之所以说大于0而不是说等于1是因为可能存在重入的情况。你可以把state变量当做是当前持有该锁的线程数量。
我们先分析独占锁,独占锁,同一时刻,锁只能被一个线程所持有的。通过state变量是否为0,我们可以分辨当前锁是否被占用,但光知道锁是不是被占用是不够的,我们并不知道占用锁的线程是哪一个。在监视器锁中,我们用ObjectMonitor对象的owner属性记录了当前拥有监视器锁的线程,而在AQS中,我们将通过exclusiveOwnerThread属性:
private transient Thread exclusiveOwnerThread;
exclusiveOwnerThread
属性的值即为当前持有锁的线程。
AQS中,队列的实现是一个双向链表,被称为sync queue
,它表示所有等待锁的线程的集合。
在并发编程中使用队列通常是将当前线程包装成某种类型的数据结构扔到等待队列中,我们来看一看AQS中队列中的每一个节点是怎么构成的:
static final class Node {
// 标记表示节点正在共享模式中等待
static final Node SHARED = new Node();
// 标记表示节点正在独占模式下等待
static final Node EXCLUSIVE = null;
// 表示线程已经被取消
// 同步队列中的线程因为超过或中断,需要从同步队列中取消。被取消的节点将不会有任何改变
static final int CANCELLED = 1;
// 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消
// 将会通知后继节点,使后继节点得以运行。
static final int SIGNAL = -1;
/*
节点在等待队列中,节点线程等待在Condition上,当其它线程对Condition调用了signal()方法后,
该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取
*/
static final int CONDITION = -2;
/**
下一次共享模式同步状态获取将会无条件的被传播下去
*/
static final int PROPAGATE = -3;
// 等待状态,对于正常的同步节点,它的初始化值为0,对于条件节点,它的初始值为CONDITION。
// 该属性使用CAS进行修改。
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
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;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
结构看起来很复杂,其实属性只有4类。
// 节点所代表的线程
volatile Thread thread;
// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用
volatile Node prev;
volatile Node next;
// 线程所处的等待锁的状态,初始化时,该值为0
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 该属性用于条件队列或者共享锁
Node nextWaiter;
注意,在这个Node类中也有一个状态变量waitStatus,它表示了当前Node所代表的线程的等待锁的状态,在独占锁模式下,我们只需要关注CANCELLED SIGNAL
两种状态即可。这里还有一个nextWaiter
属性,它在独占锁模式下永远为null。仅仅起到一个标记作用,没有实际意义。
说完队列中的节点,我们接着说回这个sync queue
,AQS是怎么使用这个队列的呢,既然是双向链表,操纵它自然只需要一个头结点和一个尾结点:
// 头结点,不代表任何线程,是一个哑结点
private transient volatile Node head;
// 尾节点,每一个请求锁的线程会加到队尾
private transient volatile Node tail;
到这里,我们就了解到了这个sync queue
的全貌:
在继续往下之前,我们再对着上图总结一下Node节点各个参数的含义:
- thread:表示当前Node所代表的线程。
- waitStatus:表示节点所处的等待状态,共享锁模式下只需要关注三种状态:
SIGNAL CANCELLED 初始态(0)
- prev next:节点的前驱和后继
- nextWaiter:作为标记,值为null时,表示当前处于独占锁模式。
AQS核心属性
AQS中,锁相关的属性有两个:
private volatile int state; //锁的状态
private transient Thread exclusiveOwnerThread; // 当前持有锁的线程,注意这个属性是从AbstractOwnableSynchronizer继承而来
sync queue(等待队列)相关的属性有两个:
private transient volatile Node head; // 队头,为dummy node
private transient volatile Node tail; // 队尾,新入队的节点
队列中的Node需要关注的属性有三组:
// 节点所代表的线程
volatile Thread thread;
// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用
volatile Node prev;
volatile Node next;
// 线程所处的等待锁的状态,初始化时,该值为0
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
FairSync in ReentrantLock
前面我们已经提到过,AQS大多数情况下是通过继承来使用的,子类通过重写tryAcquire
来实现自己获取锁的逻辑,我们这里以ReentrantLock
为例来说明锁的获取流程。
值得注意的是,ReentrantLock有公平锁
和非公平锁
,这体现在它的构造函数中:
public class ReentrantLock implements Lock, java.io.Serializable {
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync{
...
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
...
}
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 获取锁
public void lock() {
sync.lock();
}
...
}
可以看出,FairSync
继承自Sync
,而Sync
继承自AQS
,ReentrantLock获取锁的逻辑视直接调用了SyncFair
或者NonfairSync
的逻辑。
这里,我们直接以FairLock为例,来逐行分析锁的获取:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//获取锁
final void lock() {
acquire(1);
}
...
}
lock方法调用acquire
方法来自父类AQS。
我们首先给出获取锁的流程图,再逐行分析代码。
acquire()
acquire定义在AQS类中,描述了获取锁的流程
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看出,该方法涉及了四个方法的调用:
(1) tryAcquire(arg):该方法继承AQS的子类实现,为获取锁的具体逻辑。
(2) addWaiter(Node mode):该方法由AQS实现,负责在获取锁失败后调用,将当前请求锁的线程包装成Node扔到sync queue中,并返回这个Node。
(3) acquireQueued(final Node node,int arg):该方法由AQS实现,比较复杂,主要对上面刚加入队列的Node不断尝试以下两种操作之一:
- 在前驱节点就是head节点的时候,继续尝试获取锁
- 将当前线程挂起 使CPU不再调度它。
(4) selfInterrupt():该方法由AQS实现,用于中断当前线程。由于在整个抢锁过程中,我们都是不响应中断的。那如果在抢锁的过程中发生了中断怎么办呢,总不能假装看不见呀。AQS的做法简单的记录有没有发生过中断,如果返回的时候发现曾经发生过中断,则在退出acquire
方法之前,就调用selfInterrupt
自我中断一下,就好像将这个发生在抢锁过程中的中断 推迟 到抢锁结束后再发生一样。
从上面的简单介绍中可以看出,除了获取锁的逻辑tryAcquire(arg)
由子类实现外,其余方法均由AQS实现。
接下来,我们重点来看FairSync
所实现的获取锁的逻辑。
tryAcquire()
tryAcquire()
获取锁的逻辑其实很简单--------判断当前所有没有被占用:
- 如果锁没有被占用,尝试以公平的方式获取锁
- 如果锁已经被占用,检查是不是锁重入
如果锁成功返回true
,失败则返回false
。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 首先获取当前锁的状态
int c = getState();
// c == 0,说明当前锁没有被任何线程占用,可以尝试获取
// 因为实现的是公平锁,所以在强战之前首先看看队列中有没有排在自己前面的Node
// 如果没有人在排队,则通过CAS方式获取锁
// hasQueuedPredecessors()方法实际上是检查当前线程是否在head节点的后面,即第一个线程。
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果 c > 0,说明锁已经被占用了
// 对于可重入锁,这个时候检查占用锁的线程是不是就是当前线程
// 如果是的话,说明已经拿到了锁,直接重入即可。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 到这,说明有人占用了锁,并且占用锁的线程不是当前线程,则锁获取失败。
return false;
}
}
从这里可以看出,获取锁其实主要就是干一件事:
将state的状态通过CAS操作由0改为1.
由于是CAS操作,必然是只有一个线程能执行成功。则执行成功的线程获取了锁,在这之后,才有权利将exclusiveOwerThread
的值设成自己。另外,对于可重入锁,如果当前线程已经是获取了锁的线程,它还要注意增加锁的重入次数。
值得一体的是,这里修改state状态的操作,一个用了CAS方法compareAndSetState
,一个用了普通的setState
。这是因为用CAS操作时,当前线程还没有获得锁,所以可能存在多个线程同时竞争锁的情况,而调用setState()时,是在当前线程已经持有锁的情况下,所以对state的修改是安全的,只需要普通的方法就可以了。
addWaiter()
如果执行到此方法,说明前面尝试获取锁的tryAcquire
已经失败了,既然获取锁已经失败了,就要将当前线程包装成Node,加入到等待锁的队列中,因为是FIFO先入先出
队列,所以自然是直接加在队尾。
方法调用为:
addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //将当前线程包装成Node
// 这里我们用注释的形式把Node的构造函数贴出来
// 因为传入的mode值为Node.EXCLUSIVE,所以节点的nextWaiter属性被设为null
/*
static final Node EXCLUSIVE = null;
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
*/
Node pred = tail;
// 如果队列不为空, 则用CAS方式将当前节点设为尾节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 代码会执行到这里, 只有两种情况:
// 1. 队列为空
// 2. CAS失败
// 注意, 这里是并发条件下, 所以什么都有可能发生, 尤其注意CAS失败后也会来到这里
// enq():通过CAS+自旋的方式确保该节点入队。
enq(node); //将节点插入队列
return node;
}
可见,每一个处于独占锁模式下的节点,它的nextWaiter
一定是null。在这个方法中,我们首先会尝试直接入队,但是目前因为是在并发条件下,所以有可能同一时刻,有多个线程在尝试入队,导致compareAndSetTail(pred,node)操作失败-------因为有可能其他线程已经成为了新的尾结点,导致尾结点不再是我们之前看到的那个pred了。
如果入队失败了,接下来,我们就需要调用enq(node)方法,在该方法中通过自旋 + CAS
的方式,确保当前节点入队。
enq():
能执行到这个方法,说明当前线程获取锁已经失败了,我们已经把它包装成一个Node,准备把它扔到队列中去,但是在这一步又失败了,失败的原因可能是以下两种之一:
- 等待队列是空的,没有线程在等待。
- 其他线程在当前线程入队的过程中率先完成了入队,导致尾节点的值已经改变了,CAS操作失败。
在该方法中,我们使用了死循环,即以自旋的方式将节点插入队列,如果失败则不停地尝试,直到成功为止,另外,该方法也负责在队列为空时,初始化队列,这也说明,队列是延时初始化的。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果是空队列, 首先进行初始化
// 这里也可以看出, 队列不是在构造的时候初始化的, 而是延迟到需要用的时候再初始化, 以提升性能
if (t == null) {
// 注意,初始化时使用new Node()方法新建了一个dummy节点
if (compareAndSetHead(new Node()))
tail = head; // 这里仅仅是将尾节点指向dummy节点,并没有返回
} else {
// 到这里说明队列已经不是空的了, 这个时候再继续尝试将节点加到队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里尤其注意:当队列为空时我们初始化队列并没有使用当前传进来的节点,而是:
新建了一个空节点!!!
新建了一个空节点!!!
新建了一个空节点!!!
在新建完空的头节点后,我们并没有立即返回,而是将尾节点指向当前节点的头节点,然后进入下一轮循环,在下一轮循环中,尾节点已经不为null了,此时再将我们包装了当前节点的Node加入到空节点的后面。
这就意味着,在这个等待队列中,头节点是一个"哑节点",不代表任何线程。即: head节点不代表任何线程,它就是一个空节点!
尾分叉: enq方法中有一个比较有趣的现象,叫做尾分叉。我们着重看将当前节点设置成尾节点的操作:
} else {
// 到这里说明队列已经不是空的了, 这个时候再继续尝试将节点加到队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
与将大象放到冰箱里需要三步一样,将一个节点node添加到sync queue
的末尾也需要三步:
- 设置node的前驱节点为当前的尾节点:
node.prev = t
- 修改
tail
属性,使它指向当前节点 - 修改原来的尾节点,使它的next指向当前节点。
但需要注意的是,这三步并不是原子操作,第一步很容易成功;而第二步由于是一个CAS操作,在并发条件下可能失败,第三步只有在第二步成功的条件下才执行。这里的CAS保证了同一时刻只有一个节点能成为尾节点,其他节点将失败,失败后将回到for循环中继续重试。
所以,当有大量的线程在同时入队的时候,同一时刻,只有一个线程能玩征地完成这三步,而其他线程只能完成第一步,于是出现了尾分叉:
注意,这里第三步是在第二步执行成功后才执行的,这就意味着,有可能即使我们已经完成了第二步,将新的节点设置成了尾节点,此时原来旧的尾节点的next值可能还是null(因为还没有来得及执行第三步),所以如果此时又线程恰巧从头节点开始向后遍历整个链表,则它是遍历不到新加进来的尾节点的,这显然是不合理的。因为现在的tail已经指向了新的尾节点。
另一方法,当我们完成了第二步之后第一步一定是成功了的,所以如果我们从尾节点开始向前遍历,已经可以遍历到所有的节点。这也就是为什么我们在AQS相关的源码中,有时候常常会出现从尾节点开始逆向遍历链表------因为一个结点要能入队,则他的prev属性一定是有值的,但它的next属性可能暂时还没有值。
至于那些 “分叉” 的入队失败的其他节点,在下一轮的循环中,它们的prev属性会重新指向新的尾节点,继续尝试新的CAS操作,最终,所有节点都会通过自旋不断地尝试入队,直到成功为止。
addWaiter的总结:
至此,我们就完成了addWaiter(Node.EXCLUSIVE)方法的完整的分析,该方法并不设计到任何关于锁的操作,它就是解决了并发条件下的节点入队问题。具体来说就是该方法保证了将当前线程包装成Node节点加入到等待队列的队尾如果队列为空,则会新建一个哑节点作为头节点,再将当前节点接在头结点的后面。
addWaiter(Node.EXCLUSIVE)
方法最终返回了代表当前线程的Node节点,在返回的那一刻,这个节点必然是当时的sync queue
的尾节点。
我们再回到获取锁的逻辑中:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当addWaiter(Node.EXCLUSIVE)执行完毕后,节点现在已经被成功添加到sync queue
中了,接下来将执行acquireQueued方法。
acquireQueued()
该方法也是一个很难啃的方法,看代码前,首先明确几点:
- 能执行到该方法,说明
addWaiter
方法已经成功将包装了当前Thread的节点添加到了等待队列的队尾 - 该方法中将再次尝试获取锁
- 在再次尝试获取锁失败后,判断是否需要把当前线程挂起。
为什么前面获取锁失败了,这里还要再次尝试获取锁呢?
首先,这里再次尝试获取锁是基于一定的条件的,即:
当前节点的前驱节点就是HEAD节点
因为我们知道,head节点就是个哑节点,他不代表任何线程,如果当前节点的前驱节点就是head节点,那就说明当前节点已经是排在整个队列最前面的了。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 在当前节点的前驱就是HEAD节点时, 再次尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//在获取锁失败后, 判断是否需要把当前线程挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
注意这里又来了个自旋操作,我们一段段看:
final Node p = node.predecessor();
// 在当前节点的前驱就是HEAD节点时, 再次尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
首先获取尾结点的前驱节点(因为上一步addWaiter
中返回的就是尾节点,并且这个节点就是代表了当前线程的Node)。
如果前驱节点就是head节点,那说明当前线程已经排在了队列的最前面,所以这里我们再试着去获取锁。如果这一次获取成了,tryAcquire
方法返回true,则我们将进入if代码块,调用setHead
方法:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
这个方法将head指向传进来的node,并且将node的threead和prev属性置为null,如下图所示:
可以看出,这个方法的本质是丢弃原来的head,将head指向已经获得了锁的node,但是接着又将该node的thread置为null了,这在某种意义上导致了这个新的head节点又成为了一个哑节点,它不代表任何线程。为什么要这么做呢,因为在tryAcquire调用成功后,exclusiveOwnerThread
属性就已经记录了当前获取锁的线程了,此处没有必要再记录。这某种程度上就是将当前线程从等待队列里面拿出来了,是一个变相的出队操作
还有另外一个特点是,这个setHead
方法只是个普通方法,并没有像之前enq方法中那样采用compareAndSetHead方法,这是为什么呢?同我们之前分析setState
一样,因为这里不会产生竞争!
在enq方法中,当我们设置头节点的时候,是新建一个哑节点,并将它作为头节点,这个时候,可能多个线程都在执行这一步,因此我们需要通过CAS操作保证只有一个线程能成功。在acquireQueued方法里,由于我们在调用到setHead
时,已经通过tryAcquire
方法获得了锁,这意味着:
- 此时没有线程在创建新的头节点-----因为很明显此时队列并不是空的,不会执行到创建头节点的代码
- 此时能执行setHead的只有一个线程-----因为要执行到setHead,必然是tryAcquire已经返回true,而同一时刻,只有一个线程能获取到锁。
综上,在整个if语句内的代码即使不加锁,也是线程安全的。
接下来我们来看一看另一种情况,即p == head && tryAcquire(arg)返回了false,此时我们需要判断是否将当前线程挂起:
showParkAfterFailedAcquire()
从函数名也可以看出,该方法用于决定在获取锁失败后,是否将线程挂起.
决定的依据就是前驱节点的waitStatus
值。
(有没有发现一直到现在,前面的分析中我们都没有用到waitStatus
的值,终于在这里要用到了)
我们先来回顾一下waitStatus
有哪些状态值:
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
一共有四种状态,但是我们在开篇的时候就说过,在独占锁锁的获取操作中,我们只用到了其中两个 -----CANCELLED
和SIGNAL
。
当然,前面我们在创建节点的时候并没有给waitStatus
赋值,因此每一个节点最开始的waitStatus
的值都被初始化为0,即不属于上面任何一种状态。
那么CANCELLED
和SIGNAL
代表什么意思呢?
CANCELLED
状态很好理解,它表示Node所代表的当前线程已经取消了排队,即放弃获取锁了。
SIGNAL
这个状态就有点意思了,它不是表示当前节点的状态,而是表示当前节点的下一个节点的状态。
当一个节点的waitStatus
被置为SIGNAL
,就说明它的下一个节点(即它的后继节点)已经被挂起(或者马上就要被挂起了),因此在当前节点释放了锁或者放弃获取锁时,如果它的waitStatus
属性设置为SIGNAL
,则当前节点还需要完成一个额外的操作-----唤醒它的后继节点。
有意思的是,SIGNAL
这个状态的设置常常不是节点自己给自己设置的,而是后继节点设置的。给大家打个比方:
比如说出去吃饭,在人多的时候经常要排队区号,你取到了8号,前面还有7个人在等着进去,你就和排在你前面的7号讲“哥们,我现在拍在你后面,队伍这么长,一时半会也轮不到我,我去那边打个盹,一会轮到你进去了(release)或者你不想等了(cancel),麻烦你都叫醒我”,说完,你就把他的waitStatus
的值设成了SIGNAL
。
换个角度将,当我们决定要将一个线程挂起之前,首先要确保自己的前驱结点的waitStatus
为SIGNAL
,这就相当于给自己设一个闹钟再去睡,这个闹钟会在恰当的时候叫醒自己,否则,如果一直没有人来叫醒自己,自己可能就一直睡到天荒地老了。
理解了SIGNAL
和CANCELLED
这两个状态的含义后,我们再来看看shouldParkAfterFailedAcquire
是怎么用的:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 获得前驱节点的ws
if (ws == Node.SIGNAL)
// 前驱节点的状态已经是SIGNAL了,说明闹钟已经设了,可以直接睡了
return true;
if (ws > 0) {
// 当前节点的 ws > 0, 则为 Node.CANCELLED 说明前驱节点已经取消了等待锁(由于超时或者中断等原因)
// 既然前驱节点不等了, 那就继续往前找, 直到找到一个还在等待锁的节点
// 然后我们跨过这些不等待锁的节点, 直接排在等待锁的节点的后面 (是不是很开心!!!)
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱节点的状态既不是SIGNAL,也不是CANCELLED
// 用CAS设置前驱节点的ws为 Node.SIGNAL,给自己定一个闹钟
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
可以看出,shouldParkAfterFailedAcquire所做的事情无外乎:
- 如果前驱节点的
waitStatus
值为Node.SIGNAL
,则直接返回true - 如果前驱节点的
waitStatus
值为Node.CANCELLED
(ws>0),则跳过那些节点,重新寻找正常等待中的前驱节点,然后排在它后面,返回false。 - 其他情况,将前驱节点的状态改为
Node.SIGNAL
,返回false
注意了,这个函数只有在当前节点的前驱节点的waitStatus
状态本身是SIGNAL的时候才会返回true,其他是否都会返回false,我们再回到这个方法的调用处:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 我们在这里!在这里!!在这里!!!
// 我们在这里!在这里!!在这里!!!
// 我们在这里!在这里!!在这里!!!
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看出,当shouldParkAfterFailedAcquire返回false后,会继续回到循环中再次尝试获取锁-----这是因为此时我们的前驱结点可能已经变了(搞不好前驱结点就变成了head节点了呢)
当shouldParkAfter返回true,即当前节点的前驱结点的waitStatus
状态已经设为SIGNAL
后,我们就可以安心的将当前线程挂起了,此时我们将调用parkAndCheckInterrupt
:
parkAndCheckInterrupt()
到这个函数已经是最后一步了,就是将线程挂起,等待被唤醒
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 线程被挂起,停在这里不再往下执行了
return Thread.interrupted();
}
注意,LockSupport.park(this)执行完成后,线程就被挂起了,除非其他线程unpark
了当前线程,或者当前线程被中断了,否则代码不会往下执行。Thread.interrupted()是将该线程的中断标志位清除。
总结
- AQS中用state属性表示锁,如果能成功将state属性通过CAS操作从0设置成1,即获取了锁。
- 获取了锁的线程才能将exclusiveOwnerThread设置成自己
- addWaiter负责将当前等待锁的线程包装成Node,并成功地添加到队列的末尾,这一点是由它调用的enq()保证的,enq方法同时还负责队列为空时初始化队列
- acquireQueued方法用于在Node成功入队后,继续尝试获取锁(取决去Node的前驱节点是不是head),或者将线程挂起
- shouldParkAfterFailedAcquire方法用于保证当前线程的前驱节点的
waitStatus
属性值为SIGNAL
,从而保证自己挂起后,前驱节点会负责在适合的时候唤醒自己。 - parkAndCheckInterrupt方法用于挂起线程,并检查中断状态。
- 如果最终成功获取了锁,线程会从lock()方法返回,继续往下执行,否则线程阻塞等待。