AQS(AbstractQueuedSynchronizer)是整个JUC并发框架的基础,其实现大量依赖了乐观锁(CAS+自旋)的方式,而获取锁/释放锁同步器状态则依靠子类实现。若子类未实现方法,而调用了则会抛出异常。
核心思想:
如果被请求的共享资源空闲,则将请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态。如果请求共享资源被锁定,那么就需要一套阻塞线程以及唤醒线程的机制,AQS是用CLH队列来实现的,即将获取不到资源的线程加入到双向队列中。
同步器有一个state,它代表着当前同步器的状态,它是整个AQS的核心属性。我们平时使用的JUC框架下的常用类比如ReentrantLock,其实它们的方法就是在设置和改变这个state。而之前说的子类需要实现的方法,简单的说,它的实现逻辑也就是在设置和改变这个state。
private volatile int state;
如上,state已经被我们找到了,它一个volatile的变量,这样就保证了可见性。
由于本文分析的是独占锁,所以当state为0时,代表没有线程持有锁。当state为1时,代表有线程持有锁。当state>1时,代表有线程持有该锁,并且重入过该锁。所以state是否为0,可以作为判断是否有线程持有该独占锁的标准。
private transient Thread exclusiveOwnerThread;
而exclusiveOwnerThread成员则用来记录当前持有锁的线程。在其父类AbstractOwnableSynchronizer中的字段。
AQS中已经为我们实现了一个FIFO的等待队列,它是一个双向链表。由于同步器的state一般不能让所有线程同时获得,所以将这些需要暂时等待的线程包装成一个节点放到队列中去,当获取state的条件满足时,会将这个节点内的线程唤醒,以便它接下来去尝试获取state。
队列是用Node节点来实现的,如下所示:
static final class Node {
//节点为共享节点
static final Node SHARED = new Node();
//节点为独占节点
static final Node EXCLUSIVE = null;
//1 代表线程被取消
static final int CANCELLED = 1;
//-1 阻塞/唤醒后继节点的信号
static final int SIGNAL = -1;
// CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
static final int CONDITION = -2;
// PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
static final int PROPAGATE = -3;
//节点的状态 也就是上面的四个值
volatile int waitStatus;
//双向队列前驱
volatile Node prev;
//双向队列后继
volatile Node next;
//当前节点持有的线程
volatile Thread thread;
//用来表明当前node的线程是想要获取共享锁还是独占锁
Node nextWaiter;
//是否是共享节点
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回节点前驱,为null报错
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;
}
}
AQS头节点与尾节点
// 头结点,固定是一个dummy node,因为它的thread成员固定为null,换句话说,头节点就是持有锁的线程的节点
private transient volatile Node head;
// 尾节点,请求锁失败的线程,会包装成node,放到队尾
private transient volatile Node tail;
head和tail都是AQS的成员,分别代表队列的头和尾,通过持有这两个成员,相当于AQS也持有了这个队列。
等待队列如上图所示,注意head节点作为一个dummy node,它的thread成员一定为null,说白了head节点就是持有锁的节点
cas是乐观锁的一种实现方式,CAS是一种轻量级的并发处理,在对AQS属性以及队列节点属性进行修改时,都会用到CAS操作。既然是多线程环境下,那么就会有多个线程同时修改这些属性,而CAS保证了同一时刻只有一个线程能修改成功,而其他CAS操作失败的线程,一般则会通过自旋继续尝试CAS操作。
CAS是利用Unsafe工具类来完成CAS的操作的,主要针对一下属性:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
//继承了 AQS,公平锁和非公平锁又继承了Sync来使用AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
static final class NonfairSync extends Sync{
...
}
static final class FairSync extends Sync {
...
}
// 默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据参数,设置公平或非公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 获取锁
public void lock() {
sync.lock();
}
...
}
可以看到ReentrantLock使用AQS都是通过使用子类:
获取锁的第一步是
final void lock() {
//AQS中的方法
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
前面提到,tryAcquire在ReentrantLock里,NonfairSync和FairSync分别实现了tryAcquire,而我们这里从FairSync的tryAcquire看起。
简单的来说,公平锁里的tryAcquire实现就是先查看是否有线程已经在等待获取锁了,如果有则返回false,若没有则尝试设置锁给当前线程。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
1、判断是否有其他线程在排队
2、通过cas直接设置state,让当前线程持有锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
3、可重入锁,当前线程已经持有锁了,又获取了锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
tryAcquire的逻辑总结来说:
既然执行到了addWaiter,说明当前线程第一次执行tryAcquire时失败了。既然获取锁失败了,那么就需要将当前线程包装一个node,放到等待队列的队尾上去,以后锁被释放时别人就会通过这个node来唤醒自己。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
1、使用enq的快捷方法,如果CAS操作失败,才会去执行enq
Node pred = tail;
2、如果队列不为空
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
3、 执行到这里,有两种情况:
3.1.队列为空。head和tail成员从来没有初始化过
3.2.CAS操作失败。当执行compareAndSetTail时,tail成员已经被修改了
enq其实是一个自旋操作。
enq(node);
return node;
}
由于当初这么调用的addWaiter(Node.EXCLUSIVE),然后会调用下面的这版构造器。由此可见,刚放到队尾的node,它的nextWaiter肯定为null,它的waitStatus为0(默认初始化)。
static final Node EXCLUSIVE = null;
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
由于addWaiter的时候并没有成功将刚创建的node放到队尾,这时需要enq出马了。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果tail为null,说明队列为空,head肯定也为null
// 进一步说明 等待队列的初始化是懒汉式的
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
// 如果tail不为null,说明队列至少有一个dummy node,head肯定也不为null
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
1、注意,如果队列从来没有初始化过(head、tail为null),那么这个循环至少得执行两次,第一次给队列新建一个空node,第二次进if (t == null)的else分支,把参数node放在空node的后面。
2、根据上一条可知,就算只有一个线程入队,入队完毕后队列将有两个node,第一个node称为dummy node,因为它的thread成员为null;第二个node才算是实际意义的队头,它的thread成员不为null。
3、新建的是空node,它的所有成员都是默认值。thread成员为null,waitStatus为0。之后你会发现,队尾node的waitStatus总是0,因为默认初始化。
4、compareAndSetHead作为一个CAS操作只有一个参数,是因为它的实现是unsafe.compareAndSwapObject(this, headOffset, null, update);。
compareAndSetHead的CAS操作也可能失败,当队列为空时,两个线程同时执行到enq。
在队列已经有node的情况下,如果同时有多个线程执行到node.prev = t这里,最终只有一个线程能够成功设置tail成员。
} else {
node.prev = t; // 1
if (compareAndSetTail(t, node)) { // 2
t.next = node; // 3
return t;
}
}
从上面看,发现要完成双向链表的指针指向,需要经过3步:
1、将参数node的前驱指向tail。
2、CAS设置参数node为tail。
3、如果CAS成功,则修正tail的后继。
因为CAS操作的保证,所有线程都能够执行第1步,但第2、3步只会有一个线程能够执行到。
**enq的尾分叉:**从上面的步骤可以看出,如果存在很多个线程都刚好执行到了node.prev = t这里,那么CAS失败的线程不能成功入队,此时它们的prev还暂时指向的旧tail。
**prev的有效性:**从上图第二步可以看到,此时线程1的node已经是成功放到队尾了,但此时队列却处于一个中间状态,前一个node的next还没有指向队尾呢。此时,如果另一个线程如果通过next指针遍历队列,就会漏掉最后那个node;但如果另一个线程通过tail成员的prev指针遍历队列,就不会漏掉node了。
prev的有效性也解释了AQS源码里遍历队列时,为什么常常使用tail成员和prev指针来遍历,比如你看unparkSuccessor方法。
acquireQueued函数是整个独占锁获取的精华部分,它解释了独占锁获取的整个过程。
执行到这个函数,说明:
简单的说,acquireQueued是利用自旋+tryAcquire“不断”地尝试获取锁,当然实际上如果真的是在死循环来不断调tryAcquire,肯定电脑都卡死了。实际流程是:
如果尝试获取锁成功,那么函数的使命就达到了,执行完相应收尾工作,然后返回。
final boolean acquireQueued(final Node node, int arg) {
//成功失败标志
boolean failed = true;
try {
//线程中断标志
boolean interrupted = false;
for (;;) {
1、获取node的前驱
final Node p = node.predecessor();
2、如果前驱为head节点,并获取锁成功,可以获取所得前提是node的前驱是head节点
if (p == head && tryAcquire(arg)) {
2.1、将node设置为head节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
3、执行到这个位置说明node前驱不是头节点,或没有获取到锁
3.1、shouldParkAfterFailedAcquire方法作用:判断node节点是否需要阻塞
3.2、parkAndCheckInterrupt阻塞线程,唤醒之后返回线程是否中断过
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
问题1: 为什么tryAcquire(arg)的前提是p == head?
因为从enq的逻辑可知,head只会是一个dummy node,实际意义的node只会在head的后面。而node的前驱是head(final Node p = node.predecessor()),则代表node已经是队列中的第一个实际node了,排在最前面的node自然可以尝试去获取锁了。
问题2: 回想整个调用过程,是最开始在acquire里调用tryAcquire就已经失败了,然而此时第一次循环时,又可能马上去调tryAcquire(说可能,是因为需要p == head成立),这会不会是一次肯定失败的tryAcquire?
考虑这种场景,线程1获取了锁正在使用还没释放,此时队列为空,线程2此时也来获取锁,自然最开始在acquire里调用tryAcquire会失败,假设线程2刚开始执行acquireQueued,此时线程1释放了锁,此时线程2肯定排在head后面,那么线程2马上tryAcquire,然后就可以获取成功。
问题3: 执行acquireQueued的线程是谁?
一定是node参数的thread成员,虽然执行过程中,可能会经历不断 阻塞和被唤醒 的过程。
问题4: 为什么刚执行完addWaiter方法时,才把代表当前线程的node放到队尾,怎么之后一判断就会发现自己处于head的后继了?
这个问题不考虑上面的特殊场景,而考虑addWaiter时,队列中有许多node。其实这很合理,这说明从head到当前方法栈中的node之间的那些node,它们自己也会在执行acquireQueued,它们依次执行成功(指p == head && tryAcquire(arg)成功),每次执行成功相当于把head成员从队列上后移一个node,当它们都执行完毕,当前方法栈中的node自然也就是head的后继了。
注意,“之间的那些node”的最后一个node执行acquireQueued成功后(代表 最后一个node的代表线程获得锁成功,它自己成为了head),当前方法还在阻塞之中,只有当这“最后一个node”释放独占锁时,才会执行unparkSuccessor(head),当前线程才会被唤醒。关于释放独占锁,以后再具体讲。
问题5: finally块是否会执行cancelAcquire(node)?
虽然号称此函数是不响应中断的函数,但不响应中断只是对于AQS的使用者来说,如果一个线程阻塞在parkAndCheckInterrupt这里,别的线程来中断它,它是会马上唤醒的,然后继续这个循环。不过想要退出这个函数,只有通过return interrupted,而前一句就是failed = false,所以finally块里,是永远不会去执行cancelAcquire(node)的。
从逻辑中可见,如果当前线程获取锁成功,代表当前线程的node会新设置自己为head,然后将其弄成dummy node,即把node的thread成员清空,但这个被清空的thread成员已经在tryAcquire里将这个thread设置为了exclusiveOwnerThread。
我们先把函数名分成两部分:
shouldPark:指的是 该函数的返回值影响是否可以执行parkAndCheckInterrupt函数。因为
shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()这么写,
只有当该函数返回true时,才会去执行parkAndCheckInterrupt。注意,parkAndCheckInterrupt
函数执行后,当前线程就会被挂起了,也就是我们所说的阻塞。
AfterFailedAcquire:指的是 获取锁失败了才会执行该函数。其实具体指两种情况:
1. p == head为false,即当前线程的node的前驱不是head
2. 虽然 p == head为true,但tryAcquire返回false了,即当前线程虽然已经排到等待队列
的最前面,但获取锁还是失败了。
回想之前讲过的函数,发现它们居然都没有使用过waitStatus这个属性,而现在就可以派上用场了。
node一共有四种状态,但在独占锁的获取和释放过程中,我们只可能将node的状态变成CANCELLED或SIGNAL,而shouldParkAfterFailedAcquire函数就会把一个node的状态变成SIGNAL。注意,一个node新建的时候,它的waitStatus是默认初始化为0的。
本函数,简单的讲就是,跳过无效前驱,把node的有效前驱(有效是指node不是CANCELLED的)找到,并且将有效前驱的状态设置为SIGNAL,之后便返回true代表马上可以阻塞了。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 前驱节点已经设置了SIGNAL,闹钟已经设好,现在我可以安心睡觉(阻塞)了。
* 如果前驱变成了head,并且head的代表线程exclusiveOwnerThread释放了锁,
* 就会来根据这个SIGNAL来唤醒自己
*/
return true;
if (ws > 0) {
/*
* 发现传入的前驱的状态大于0,即CANCELLED。说明前驱节点已经因为超时或响应了中断,
* 而取消了自己。所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 进入这个分支,ws只能是0或PROPAGATE。
* CAS设置ws为SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
由于shouldParkAfterFailedAcquire函数在acquireQueued的调用中处于一个死循环中,且因为shouldParkAfterFailedAcquire函数若返回false,且考虑当前线程一直不能获取到锁的情况,那么此函数必将至少执行两次才能阻塞自己。
既然shouldParkAfterFailedAcquire已经返回了true,那么我现在可以执行parkAndCheckInterrupt了。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //阻塞当前线程
return Thread.interrupted();
}
调用完LockSupport.park(this),当前线程就阻塞在这里,直到有别的线程unpark了当前线程,或者中断了当前线程。而返回的Thread.interrupted()代表当前线程在阻塞的过程中,有没有被别的线程中断过,如果有,则返回true。注意,Thread.interrupted()会消耗掉中断的状态,即第一次执行能返回true,但紧接着第二次执行就只会返回false了。
回到acquireQueued的逻辑中,发现一旦当前线程被中断过一次,那么parkAndCheckInterrupt就返回了true,那么执行interrupted = true,interrupted局部变量就一定是true的了。
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()) //这里返回了true,说明上一行也返回了true才会执行到这里
interrupted = true; //这里设置后,会被永久保留
}
} finally {
if (failed)
cancelAcquire(node);
}
}
再等到当前线程获取锁成功后,那么acquireQueued返回的就一定是true了。再回到acquire的逻辑,发现需要进入if分支,再执行selfInterrupt()将中断状态补上,这样下一次Thread.interrupted()就能返回true了。为的就是在 回到用户代码之前,把中断状态补上,万一用户需要中断状态呢。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //acquireQueued返回了true
selfInterrupt(); //中断状态补上
}
其实不公平锁与公平锁之间的差别只会体现在AQS子类的重写方法中,所以我们直接看子类实现就好。使用不公平锁对ReentrantLock来说,还是直接调用ReentrantLock.lock(),没有区别。要想使用非公平锁,必须在构造ReentrantLock时指定好。
//ReentrantLock.java
abstract static class Sync extends AbstractQueuedSynchronizer {
...
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;
}
...
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// if分支里的逻辑只是一次快速尝试
// 它和nonfairTryAcquire里的部分逻辑是一样的
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 将nonfairTryAcquire的逻辑直接放在这里,就把公平锁看起来一样了
return nonfairTryAcquire(acquires);
}
}
其实非公平锁的原理就是:不管等待队列中是否有等待线程,直接竞争获取state,要是获取成功了,就直接设置当前线程为exclusiveOwnerThread成员了(插队成功)。这不就是,插了所有等待线程的队嘛。也就是说,非公平锁它可以和队列中的head后继的代表线程同时竞争锁。
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 非公平锁插队的机会,只有这里tryAcquire的时候
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
非公平锁只有在这儿的tryAcquire中才会插队,若插队失败,其实是公平锁一样的,放入到队列中,进入acquireQueued的死循环,在acquireQueued方法中执行tryAcquire需要先判断p==head,也就是当前线程节点必须是head的后继。
总结:
使用响应中断的锁对ReentrantLock来说,是调用ReentrantLock.lockInterruptibly()。从下面函数中,可以看到函数都会抛出InterruptedException异常,这是我们能看到的第一处不同,他与lock的区别其实就是只要是线程被中断就会抛出异常。
//ReentrantLock.java
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//AbstractQueuedSynchronizer.java
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
acquireInterruptibly这个方法与 不响应中断的acquire方法 对应。同样的,进入这个方法后,会第一次进行tryAcquire尝试。但不同的,此acquireInterruptibly函数中,会去检测Thread.interrupted(),并抛出异常。
但注意,对于acquireInterruptibly这个方法而言,既可以是公平的,也可以是不公平的,这完全取决于tryAcquire的实现(即取决于ReentrantLock当初是怎么构造的)。
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())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
而doAcquireInterruptibly方法 的相对应的方法为acquireQueued方法。观察两个方法的差别很少:
acquireQueued方法需要有返回值,因为函数调用返回上层 后,需要根据返回值来判断是否需要重新设置中断状态,在返回用户代码之前。
doAcquireInterruptibly不需要返回值,因为该函数中如果检测到了中断状态,就直接抛出异常就好了。
在doAcquireInterruptibly方法中,如果线程阻塞在parkAndCheckInterrupt这里后,别的线程来中断阻塞线程,阻塞线程会被唤醒,然后抛出异常。本来抛出异常该函数就马上结束掉的,但由于有finally块,所以在结束掉之前会去执行finally块,并且由于failed为true,则会执行cancelAcquire(node)。
回忆起node有一种CANCELLED状态,看来就是为cancelAcquire(node)准备的了。
取消线程/中断线程
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
1、防止抛出异常
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
2、获取前驱节点
Node pred = node.prev;
3、waitStatus大于0说明线程被取消,过滤取消线程,找到可用线程
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.
// pred的后继无论如何都需要取消,因为即使前面循环没有执行,
// 现在pred的后继(肯定是参数node)也是一个马上取消掉的node。
// 之后有些CAS操作会尝试修改pred的后继,如果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.
// 这里直接使用赋值操作,而不是CAS操作。
// 如果别的线程在执行这步之后,别的线程将会跳过这个node。
// 如果别的线程在执行这步之前,别的线程还是会将这个node当作有效节点。
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
4、如果node是尾节点,则直接将前一个可用节点设置为尾节点,尾节点后继直接设置为null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
5、判断是否唤醒node的后继,这种情况是pred为head被取消或执行结束,否则需要把node的后继编程pred的后继
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
5.1、设置node的后继为pred的后继
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
6、唤醒下一个等待的线程
unparkSuccessor(node);
}
//帮助gc回收垃圾
node.next = node; // help GC
}
}
如果node是队尾,那说明node后面没有节点,也就不存在节点需要去唤醒了。
分析node不是队尾的情况,既然不是队尾,说明node后面有节点。如下图所示。
这里面又有分支,先直接看进入分支要做什么,再来分析进入分支的条件:
总结一下cancelAcquire:
使用超时的锁对ReentrantLock来说,是调用public boolean tryLock(long timeout, TimeUnit unit)。从下面函数中,可以看到函数都会抛出InterruptedException异常,这是我们能看到的第一处不同。
//ReentrantLock.java
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));//第二个实参,换算得到nano
}
//AbstractQueuedSynchronizer.java
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
tryAcquireNanos这个方法与 不响应中断的acquire方法 对应。同样的,进入这个方法后,会第一次进行tryAcquire尝试。但不同的,此tryAcquireNanos函数中,会先去检测Thread.interrupted(),并抛出异常。
但注意,对于tryAcquireNanos这个方法而言,既可以是公平的,也可以是不公平的,这完全取决于tryAcquire的实现(即取决于ReentrantLock当初是怎么构造的)。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)//如果时间小于0,直接返回失败
return false;
final long deadline = System.nanoTime() + nanosTimeout;//算出“死期”,当死期到了就直接返回失败了
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 true;
}
// 看看你的年龄是否到达了死期
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)//小于0,说明死期到了
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)//剩余寿命得大于1ms
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
将doAcquireNanos方法 与不响应中断的acquireQueued方法、响应中断的doAcquireInterruptibly方法进行对比,发现差别很少,简单的我用注释说明了。
只一次尝试的、非公平的独占锁获得对ReentrantLock来说,是调用ReentrantLock.tryLock()。而nonfairTryAcquire方法就是之前讲过的非公平锁的获取方式。
//ReentrantLock.java
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
...
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;
}
...
}
参考链接:https://blog.csdn.net/anlian523/article/details/106344926