AbstractQueuedSynchronizer,来自于JDK1.5,位于JUC包,由并发编程大师Doug Lea编写,字面翻译就是“抽象队列同步器”,简称为AQS。AQS作为一个抽象类,是构建JUC包中的锁(比如ReentrantLock)或者其他同步组件(比如CountDownLatch)的底层基础框架。
这个框架有几个重要的模块:
state
:同步量,所有线程是通过竞争他,来判断是否加锁成功。既然是以它为标准,那当然需要保证可见性。所以state用volatie修饰。
同步队列
:竞争同步量调用acquire失败后进入的一个等待队列,由双向链表组成。
condition
:光只有锁还不够,我们在多线程间做线程同步,有更多复杂的需求,哪怕我获取锁了,但是我还需要等待另一个线程给我加工些东西,那么我还需要进入等待。我们可以通过condition.await来进入条件队列进行等待并且释放锁,等其他线程signal该线程。
条件队列
:等待条件进入的就是条件队列,一个锁可以生成多个condition对象,也就是可以等待多种条件来进行线程同步。条件可以有很多个,条件队列也可以有很多个。这样线程间能根据不同条件进行更复杂的并发控制。
LockSuport
:获取锁失败,或者获取条件,我们线程都会进入一个队列进行阻塞等待。那么阻塞这个操作,就依赖我们的基础工具LockSuport.park来支持。
那么接下来,我们就深入了解这些模块
AbstractQueuedSynchronizer抽象类
属性
//指向同步队列的哨兵头结点 private transient volatile Node head; //指向同步队列的尾节点 private transient volatile Node tail; //最重要的同步量 private volatile int state; //用来保存当前获取锁的线程,以便后期判断。需要实现者去设置值 private transient Thread exclusiveOwnerThread;
需重写方法
/** * 独占式获取锁,该方法需要查询当前状态并判断锁是否符合预期,然后再进行CAS设置锁。返回true则成功,否则失败。 * * @param arg 参数,在实现的时候可以传递自己想要的值 * @return 返回true则成功,否则失败。 */ protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } /** * 独占式释放锁,等待获取锁的线程将有机会获取锁。返回true则成功,否则失败。 * * @param arg 参数,在实现的时候可以传递自己想要的数据 * @return 返回true则成功,否则失败。 */ protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); } /** * 共享式获取锁,返回大于等于0的值表示获取成功,否则失败。 * * @param arg 参数,在实现的时候可以传递自己想要的数据 * @return 返回大于等于0的值表示获取成功,否则失败。 * 如果返回值小于0,表示当前线程共享锁失败 * 如果返回值大于0,表示当前线程共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功 * 如果返回值等于0,表示当前线程共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败(实际上也有可能成功,在后面的源码部分会将) */ protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); } /** * 共享式释放锁。返回true成功,否则失败。 * * @param arg 参数,在实现的时候可以传递自己想要的数据 * @return 返回true成功,否则失败。 */ protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); } /** * 判断当前线程是否独占锁。比如我们调用signal方法,就需要线程独占锁才能触发条件通知 * * @return 如果同步是以独占方式进行的,则返回true;其他情况则返回 false */ protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); }
供调用方法
这个框架提供给子类直接调用的模板方法都是final修饰的,不让子类重写。
它们又分为三类:
独占方式
/** * 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回, * 否则,将会进入同步队列等待。 * 该方法不会响应中断。该方法内部调用了可重写的tryAcquire方法。 */ public final void acquire(int arg) /** *与acquire方法相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中, *如果当前被中断,则该方法会抛出InterruptedException并返回。 */ public final void acquireInterruptibly(int arg)(int arg) /** *在acquireInterruptibly方法基础上增加了超时限制, * 如果当前线程在超时时间内没有获取到同步状态, * 那么将会返回false,获取到了返回true。 */ public final boolean tryAcquireNanos(int arg,long nanos) /** *独占式的释放同步状态,该方法会在释放同步状态之后 * 将同步队列中第一个结点包含的线程唤醒。 * 该方法内部调用了可重写的tryRelease方法。 */ public final boolean release(int arg)
共享方式
/** * 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待。 * 与独占式的不同是同一时刻可以有多个线程获取到同步状态。该方法不会响应中断。 * 该方法内部调用了可重写的tryAcquireShared方法。 */ public final void acquireShared(int arg) /** * 与acquireShared (int arg) 相同,但是该方法响应中断, * 当前线程未获取到同步状态而进入同步队列中, * 如果当前被中断,则该方法会抛出InterruptedException并返回。 */ public final void acquireSharedInterruptibly(int arg) /** *在acquireSharedInterruptibly方法基础上增加了超时限制, * 如果当前线程在超时时间内没有获取到同步状态, * 那么将会返回false,获取到了返回true。 */ public final boolean tryAcquireSharedNanos(int arg,long nanos) /** *共享式释放同步状态,该方法会在释放同步状态之后,尝试唤醒同步队列中的后继节点中的线程。 * 该方法内部调用了可重写的tryReleaseShared方法。 */ public final boolean releaseShared(int arg)
获取线程等待情况
//有些子类有需求,想要获取同步队列上的所有线程,让用户好操作 public final Collection<Thread> getQueuedThreads()
加锁解锁流程
加锁失败会进入一个同步队列,我们先看看这个队列的结构
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * 当前获取锁的线程,该变量定义在父类中,AQS直接继承。在独占锁的获取时,如果是重入锁,那么需要知道到底是哪个线程获得了锁。没有就是null */ private transient Thread exclusiveOwnerThread; /** * AQS中保持的对同步队列的引用 * 队列头结点,实际上是一个哨兵结点,不代表任何线程,head所指向的Node的thread属性永远是null。 */ private transient volatile Node head; /** * 队列尾结点,后续的结点都加入到队列尾部 */ private transient volatile Node tail; /** * 同步状态 */ private volatile int state; /** * Node内部类,同步队列的结点类型 */ static final class Node { /*AQS支持共享模式和独占模式两种类型,下面表示构造的结点类型标记*/ /** * 共享模式下构造的结点,用来标记该线程是获取共享资源时被阻塞挂起后放入AQS 队列的 */ static final Node SHARED = new Node(); /** * 独占模式下构造的结点,用来标记该线程是获取独占资源时被阻塞挂起后放入AQS 队列的 */ static final Node EXCLUSIVE = null; /*线程结点的等待状态,用来表示该线程所处的等待锁的状态*/ /** * 指示当前结点(线程)需要取消等待 * 由于在同步队列中等待的线程发生等待超时、中断、异常,即放弃获取锁,需要从同步队列中取消等待,就会变成这个状态 * 如果结点进入该状态,那么不会再变成其他状态 */ static final int CANCELLED = 1; /** * 指示当前结点(线程)的后续结点(线程)需要取消等待(被唤醒) * 如果一个结点状态被设置为SIGNAL,那么后继结点的线程处于挂起或者即将挂起的状态 * 当前结点的线程如果释放了锁或者放弃获取锁并且结点状态为SIGNAL,那么将会尝试唤醒后继结点的线程以运行 * 这个状态通常是由后继结点给前驱结点设置的。一个结点的线程将被挂起时,会尝试设置前驱结点的状态为SIGNAL */ static final int SIGNAL = -1; /** * 线程在等待队列里面等待,waitStatus值表示线程正在等待条件 * 原本结点在等待队列中,结点线程等待在Condition上,当其他线程对Condition调用了signal()方法之后 * 该结点会从从等待队列中转移到同步队列中,进行同步状态的获取 */ static final int CONDITION = -2; /** * 释放共享资源时需要通知其他结点,waitStatus值表示下一个共享式同步状态的获取应该无条件传播下去 */ static final int PROPAGATE = -3; /** * 记录当前线程等待状态值,包括以上4中的状态,还有0,表示初始化状态 */ volatile int waitStatus; /** * 前驱结点,当结点加入同步队列将会被设置前驱结点信息 */ volatile Node prev; /** * 后继结点 */ volatile Node next; /** * 当前获取到同步状态的线程 */ volatile Thread thread; /** * 等待队列中的后继结点,如果当前结点是共享模式的,那么这个字段是一个SHARED常量 * 在独占锁模式下永远为null,仅仅起到一个标记作用,没有实际意义。 */ Node nextWaiter; /** * 如果是共享模式下等待,那么返回true(因为上面的Node nextWaiter字段在共享模式下是一个SHARED常量) */ final boolean isShared() { return nextWaiter == SHARED; } /** * 用于建立初始头结点或SHARED标记 */ Node() { } /** * 用于添加到等待队列 * * @param thread * @param mode */ Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } //...... } } }
它的head引用指向的头结点作为哨兵结点,不存储任何与等待线程相关的信息,或者可以当作是已经获得锁的结点。第二个结点开始才是真正的等待线程构建的结点,后续的结点会加入到链表尾部。
入尾部的操作是采用cas的方式,能保证线程安全。
线程在释放锁的时候,会调用release,子类去管理state值的重置,模板骨架会从同步队列找出最早的一个来唤醒。
独占锁加锁
通过调用AQS的acquire模版方法可以独占式的获取锁,该方法不会响应中断,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。基于独占式实现的组件有ReentrantLock等。
该方法大概步骤如下:
- 首先调用tryAcquire方法尝试获取锁,如果获取锁成功会返回true,方法结束;否则获取锁失败返回false,然后进行下一步的操作。
- 通过addWaiter方法将线程按照独占模式Node.EXCLUSIVE构造同步结点,并添加到同步队列的尾部。
- 然后通过acquireQueued(Node node,int arg)方法继续自旋获取锁。
- 一次自旋中如果获取不到锁,那么判断是否可以挂起并尝试挂起结点中的线程(调用LockSupport.park(this)方法挂起自己,注意这里的线程状态是WAITING)。而挂起线程的唤醒主要依靠前驱结点或线程被中断来实现,注意唤醒之后会继续自旋尝试获得锁。
- 终只有获得锁的线程才能从acquireQueued方法返回,然后根据返回值判断是否调用selfInterrupt设置中断标志位,但此时线程处于运行态,即使设置中断标志位也不会抛出异常(即acquire(lock)方法不会响应中断)。
- 线程获得锁,acquire方法结束,从lock方法中返回,继续后续执行同步代码!
/** * 独占式的尝试获取锁,一直获取不成功就进入同步队列等待 */ public final void acquire(int arg) { //内部是由4个方法的调用组成的 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire尝试获取独占锁
这就是子类去重写的尝试获取独占锁的方法,在很多场景会复用到该方法
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
addWaiter加入到同步队列
addWaiter方法是AQS提供的,也不需要我们重写,或者说是锁的通用方法!
addWaiter方法用于将按照独占模式构造的同步结点Node.EXCLUSIVE添加到同步队列的尾部。大概步骤为:
按照给定模式,构建新结点。
如果同步队列不为null,则尝试将新结点添加到队列尾部(只尝试一次),如果添加成功则返回新结点,方法结束。
如果队列为null或者添加失败,则调用enq方法循环尝试添加,直到成功,返回新结点,方法结束。
/** * addWaiter(Node node)方法将获取锁失败的线程构造成结点加入到同步队列的尾部 * * @param mode 模式。独占模式传入的是一个Node.EXCLUSIVE,即null;共享模式传入的是一个Node.SHARED,即一个静态结点对象(共享的、同一个) * @return 返回构造的结点 */ private Node addWaiter(Node mode) { /*1 首先构造结点*/ Node node = new Node(Thread.currentThread(), mode); /*2 尝试将结点直接放在队尾*/ //直接获取同步器的tail结点,使用pred来保存 Node pred = tail; /*如果pred不为null,实际上就是队列不为null * 那么使用CAS方式将当前结点设为尾结点 * */ if (pred != null) { node.prev = pred; //通过使用compareAndSetTail的CAS方法来确保结点能够被线程安全的添加,虽然不一定能成功。 if (compareAndSetTail(pred, node)) { //将新构造的结点置为原队尾结点的后继 pred.next = node; //返回新结点 return node; } } /* * 3 走到这里,可能是: * (1) 由于可能是并发条件,并且上面的CAS操作并没有循环尝试,因此可能添加失败 * (2) 队列可能为null * 调用enq方法,采用自旋方式保证构造的新结点成功添加到同步队列中 * */ enq(node); return node; } /** * addWaiter方法中使用到的Node构造器 * * @param thread 当前线程 * @param mode 模式 */ Node(Thread thread, AbstractQueuedSynchronizer.Node mode) { //等待队列中的后继结点 就等于该结点的模式 //由此可知,共享模式该值为Node.SHARED结点常量,独占模式该值为null this.nextWaiter = mode; //当前线程 this.thread = thread; }
enq保证结点入队
enq做两件事
1.如果没初始化,可以初始化队列,生成哨兵节点
2.循环保证一定插入成功
/** * 循环,直到尾结点添加成功 */ private Node enq(final Node node) { /*死循环操作,直到添加成功*/ for (; ; ) { //获取尾结点t Node t = tail; /*如果队列为null,则初始化同步队列*/ if (t == null) { /*调用compareAndSetHead方法,初始化同步队列 * 注意:这里是新建了一个空白结点,这就是传说中的哨兵结点 * CAS成功之后,head将指向该哨兵结点,返回true * */ if (compareAndSetHead(new Node())) //尾结点指向头结点(哨兵结点) tail = head; /*之后并没有结束,而是继续循环,此时队列已经不为空了,因此会进行下面的逻辑*/ } /*如果队列不为null,则和外面的的方法类似,调用compareAndSetTail方法,新建新结点到同步队列尾部*/ else { /*1 首先修改新结点前驱的指向,这一步不是安全的 但是没关系,因为这一步如果发生了冲突,那么下面的CAS操作必然之后有一条线程会成功 其他线程将会重新循环尝试*/ node.prev = t; /* * 2 调用compareAndSetTail方法通过CAS方式尝试将结点添加到同步队列尾部 * 如果添加成功,那么才能继续下一步,结束这个死循环,否则就会不断循环尝试添加 * */ if (compareAndSetTail(t, node)) { //3 修改原尾结点后继结点的指向 t.next = node; //返回新结点,结束死循环 return t; } } } } /** * CAS添加头结点. 仅仅在enq方法中用到 * * @param update 头结点 * @return true 成功;false 失败 */ private final boolean compareAndSetHead(Node update) { return unsafe.compareAndSwapObject(this, headOffset, null, update); } /** * CAS添加尾结点. 仅仅在enq方法中用到 * * @param expect 预期原尾结点 * @param update 新尾结点 * @return true 成功;false 失败 */ private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); }
acquireQueued结点自旋获取锁
能够走到该方法,那么说明通过了tryAcquire()和addWaiter()方法,表示该线程获取锁已经失败并且被放入同步队列尾部了。
acquireQueued方法表示结点进入同步队列之后的动作,实际上就进入了一个自旋的过程,自旋过程中,当条件满足,获取到了锁,就可以从这个自旋中退出并返回,否则可能会阻塞该结点的线程,后续即使阻塞被唤醒,还是会自旋尝试获取锁,直到成功或者而抛出异常。
最终如果该方法会因为获取到锁而退出,则会返回否被中断标志的标志位 或者 因为异常而退出,则会抛出异常!大概步骤为:
- 同样开启一个死循环,在死循环中进行下面的操作;
- 如果当前结点的前驱是head结点,那么尝试获取锁,如果获取锁成功,那么当前结点设置为头结点head,当前结点线程出队,表示当前线程已经获取到了锁,然后返回是否被中断标志,结束循环,进入finally;
- 如果当前结点的前驱不是head结点或者尝试获取锁失败,那么判断当前线程是否应该被挂起,如果返回true,那么调用parkAndCheckInterrupt挂起当前结点的线程(LockSupport.park 方法挂起线程,线程出于WAITING),此时不再执行后续的步骤、代码。
- 如果当前线程不应该被挂起,即返回false,那本次循环结束,继续下一次循环。
- 如果线程被其他线程唤醒,那么判断是否是因为中断而被唤醒并修改标志位,同时继续循环,直到在步骤2获得锁,才能跳出循环!(这也是acquire方法不会响应中断的原理—park方法被中断时不会抛出异常,仅仅是从挂起状态返回,然后需要继续尝试获取锁)
- 最终,线程获得了锁跳出循环,或者发生异常跳出循环,那么会执行finally语句块,finally中判断线程是否是因为发生异常而跳出循环,如果是,那么执行cancelAcquire方法取消该结点获取锁的请求;如果不是,即因为获得锁跳出循环,则finally中什么也不干!
/** * @param node 新结点 * @param arg 参数 * @return 如果在等待时中断,则返回true */ final boolean acquireQueued(final Node node, int arg) { //failed表示获取锁是否失败标志 boolean failed = true; try { //interrupted表示是否被中断标志 boolean interrupted = false; /*死循环*/ for (; ; ) { //获取新结点的前驱结点 final Node p = node.predecessor(); /*只有前驱结点是头结点的时候才能尝试获取锁 * 同样调用tryAcquire方法获取锁 * */ if (p == head && tryAcquire(arg)) { //获取到锁之后,就将自己设置为头结点(哨兵结点),线程出队列 setHead(node); //前驱结点(原哨兵结点)的链接置空,由JVM回收 p.next = null; //获取锁是否失败改成false,表示成功获取到了锁 failed = false; //返回interrupted,即返回线程是否被中断 return interrupted; } /*前驱结点不是头结点或者获取同步状态失败*/ /*shouldParkAfterFailedAcquire检测线程是否应该被挂起,如果返回true * 则调用parkAndCheckInterrupt用于将线程挂起 * 否则重新开始循环 * */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) /*到这一步,说明是当前结点(线程)因为被中断而唤醒,那就改变自己的中断标志位状态信息为true * 然后又从新开始循环,直到获取到锁,才能返回 * */ interrupted = true; } } /*线程获取到锁或者发生异常之后都会执行的finally语句块*/ finally { /*如果failed为true,表示获取锁失败,即对应发生异常的情况, 这里发生异常的情况只有在tryAcquire方法和predecessor方法中可能会抛出异常,此时还没有获得锁,failed=true 那么执行cancelAcquire方法,该方法用于取消该线程获取锁的请求,将该结点的线程状态改为CANCELLED,并尝试移除结点(如果是尾结点) 另外,在超时等待获取锁的的方法中,如果超过时间没有获取到锁,也会调用该方法 如果failed为false,表示获取到了锁,那么该方法直接结束,继续往下执行;*/ if (failed) //取消获取锁请求,将当前结点从队列中移除, cancelAcquire(node); } } /** * 位于Node结点类中的方法 * 返回上一个结点,或在 null 时引发 NullPointerException。 当前置不能为空时使用。 空检查可以取消,表示此异常无代码层面的意义,但可以帮助 VM?所以这个异常到底有啥用? * * @return 此结点的前驱 */ final Node predecessor() throws NullPointerException { //获取前驱 Node p = prev; //如果为null,则抛出异常 if (p == null) throw new NullPointerException(); else //返回前驱 return p; } /** * head指向node新结点,该方法是在tryAcquire获取锁之后调用,不会产生线程安全问题 * * @param node 新结点 */ private void setHead(Node node) { head = node; //新结点的thread和prev属性置空 //即丢弃原来的头结点,新结点成为哨兵结点,内部线程出队 //设置里虽然线程引用置空了,但是一般在tryAcquire方法中轨记录获取到锁的线程,因此不担心找不到是哪个线程获取到了锁 //这里也能看出,哨兵结点或许也可以叫做"获取到锁的结点" node.thread = null; node.prev = null; }
shouldParkAfterFailedAcquire结点是否应该挂起
shouldParkAfterFailedAcquire方法在没有获取到锁之后调用,用于判断当前结点是否需要被挂起。大概步骤如下:
- 如果前驱结点已经是SIGNAL(-1)状态,即表示当前结点可以挂起,返回true,方法结束;
- 否则,如果前驱结点状态大于0,即 Node.CANCELLED,表示前驱结点放弃了锁的等待,那么由该前驱向前查找,直到找到一个状态小于等于0的结点,当前结点排在该结点后面,返回false,方法结束;
- 否则,前驱结点的状态既不是SIGNAL(-1),也不是CANCELLED(1),尝试CAS设置前驱结点的状态为SIGNAL(-1),返回false,方法结束!
只有前驱结点状态为SIGNAL时,当前结点才能安心挂起,否则一直自旋!
从这里能看出来,一个结点的SIGNAL状态一般都是由它的后继结点设置的,但是这个状态却是表示后继结点的状态,表示的意思就是前驱结点如果释放了锁,那么就有义务唤醒后继结点!
/** * 检测当前结点(线程)是否应该被挂起 * * @param pred 该结点的前驱 * @param node 该结点 * @return 如果前驱结点已经是SIGNAL状态,当前结点才能挂起,返回true;否则,可能会查找新的前驱结点或者尝试将前驱结点设置为SIGNAL状态,返回false */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取 前取的waitStatus_等待状态 //回顾创建结点时候,并没有给waitStatus赋值,因此每一个结点最开始的时候waitStatus的值都为0 int ws = pred.waitStatus; /*如果前驱结点已经是SIGNAL状态,即表示当前结点可以挂起*/ if (ws == Node.SIGNAL) return true; /*如果前驱结点状态大于0,即 Node.CANCELLED 表示前驱结点放弃了锁的等待*/ if (ws > 0) { /*由该前驱向前查找,直到找到一个状态小于等于0的结点(即没有被取消的结点),当前结点成为该结点的后驱,这一步很重要,可能会清理一段被取消了的结点,并且如果该前驱释放了锁,还会唤醒它的后继,保持队列活性*/ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } /*否则,前驱结点的状态既不是SIGNAL(-1),也不是CANCELLED(1)*/ else { /*前驱结点的状态CAS设置为SIGNAL(-1),可能失败,但没关系,因为失败之后会一直循环*/ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } //返回false,表示当前结点不能挂起 return false; }
parkAndCheckInterrupt挂起线程&判断中断状态
shouldParkAfterFailedAcquire方法返回true之后,将会调用parkAndCheckInterrupt方法挂起线程并且后续判断中断状态,分两步:
- 使用LockSupport.park(this)挂起该线程,不再执行后续的步骤、代码。直到该线程被中断或者被唤醒(unpark)!
- 如果该线程被中断或者唤醒,那么返回Thread.interrupted()方法的返回值,该方法用于判断前线程的中断状态,并且清除该中断状态,即,如果该线程因为被中断而唤醒,则中断状态为true,将中断状态重置为false,并返回true,如果该线程不是因为中断被唤醒,则中断状态为false,并返回false。
/** * 挂起线程,在线程返回后返回中断状态 * * @return 如果因为线程中断而返回,而返回true,否则返回false */ private final boolean parkAndCheckInterrupt() { /*1)使用LockSupport.park(this)挂起该线程,不再执行后续的步骤、代码。直到该线程被中断或者被唤醒(unpark)*/ LockSupport.park(this); /*2)如果该线程被中断或者唤醒,那么返回Thread.interrupted()方法的返回值, 该方法用于判断前线程的中断状态,并且清除该中断状态,即,如果该线程因为被中断而唤醒,则中断状态为true,将中断状态重置为false,并返回true,注意park方法被中断时不会抛出异常! 如果该线程不是因为中断被唤醒,则中断状态为false,并返回false*/ return Thread.interrupted(); }
finally代码块
在acquireQueued方法中,具有一个finally代码块,那么无论try中发生了什么,finally代码块都会执行的。在acquire独占式不可中断获取锁的方法中,执行finally的只有两种情况:
- 当前结点(线程)最终获取到了锁,此时会进入finally,而在获取到锁之后会设置failed = false。
- 在try中发生了异常,此时直接跳到finally中。这里发生异常的情况只可能在tryAcquire或predecessor方法中发生,然后直接进入finally代码块中,此时还没有获得锁,failed=true!
a. tryAcquire方法是我们自己实现的,抛出什么异常由我们来定,就算抛出异常一般也不会在acquireQueued中抛出,可能在最开始调用tryAcquire时就抛出了。
b.predecessor方法中,会检查如果前驱结点为null则抛出NullPointerException。但是注释中又说这个检查无代码层面的意义,或许是这个异常永远不会抛出?
finally代码块中的逻辑为:
- 如果failed = true,表示没有获取锁而进行finally,即发生了异常。那么执行cancelAcquire方法取消当前结点线程获取锁的请求,acquireQueued方法结束,然后抛出异常。
- 如果failed = false,表示已经获取到了锁,那么实际上finally中什么都不会执行。acquireQueued方法结束,返回interrupted—是否被中断标志。
综上所述,在acquire独占式不可中断获取锁的方法中,大部分情况在finally中都是什么也不干就返回了,或者说抛出异常的情况基本没有,因此cancelAcquire方法基本不考虑。
但是在可中断获取锁或者超时获取锁的方法中,执行到cancelAcquire方法的情况还是比较常见的。因此将cancelAcquire方法的源码分析放到可中断获取锁方法的源码分析部分!
selfInterrupt自我中断
selfInterrupt是acquire中最后可能调用的一个方法,顾名思义,用于自我中断,什么意思呢,就是根据!tryAcquire和acquireQueued返回值判断是否需要设置中断标志位。
只有tryAcquire尝试失败,并且acquireQueued方法true时,才表示该线程是被中断过了的,但是在parkAndCheckInterrupt里面判断中断标志位之后又重置的中断标志位(interrupted方法会重置中断标志位)。
虽然看起来没啥用,但是本着负责的态度,还是将中断标志位记录下来。那么此时重新设置该线程的中断标志位为true。
/** * 中断当前线程,由于此时当前线程出于运行态,因此只会设置中断标志位,并不会抛出异常 */ static void selfInterrupt() { Thread.currentThread().interrupt(); }