【Java并发】揭秘Lock体系 -- 深入理解AbstractQueuedSynchronizer(AQS)
系列文章目录
文章目录
- 系列文章目录
- 一、 AbstractQueuedSynchronizer简介
- 二、同步队列
- 1.Node节点
- 2. 同步队列的结构
- 三、 独占锁
- 1. 独占锁的获取(acquire()方法)
- 2.独占锁的释放(release()方法)
- 3.可中断式获取锁(acquireInterruptibly() 方法)
- 4.超时等待式获取锁(tryAcquireNanos()方法)
- 四、共享锁
- 1. 共享锁的获取(acquireShared()方法)
- 2. 共享锁的释放(releaseShared()方法)
- 3.可中断式获取共享锁(acquireSharedInterruptibly()方法)
- 4.超时等待式获取共享锁(tryAcquireSharedNanos()方法)
- 总结
一、 AbstractQueuedSynchronizer简介
在同步组件的实现中,AQS是整个同步组件实现过程中所依赖的核心底层类,同步组件的实现者通过使用AQS提供的模板方法实现了同步组件的语义。AQS则实现了对同步状态的管理、对阻塞线程进行排队及等待通知等一些底层实现的处理。
AQS的核心包括两个方面:
- 维护线程阻塞等待的同步队列、独占式锁的获取和释放以及共享锁的获取;
- 释放以及可中断锁和超时等待锁获取这些特性的实现。
AOS所提供的高频使用的模板方法,归纳整理如下:
一、独占式锁
void acquire(int arg)//独占式获取同步状态,如果获取失败则插入同步队列进行等待
void acquireInterruptibly(int arg)//与acquire()方法相同,但在同步队列中进行等待时可以检测是否中断
boolean tryAcquireNanos(int arg, long nanosTimeout)//在acquireInterruptibly()方法的基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false
boolean release(int arg)//释放同步状态,该方法会唤醒在同步队列中的下一个节点
二、共享式锁
void acquireshared(int arg)//共享式获取同步状态,它与独占式的区别在于同一时刻会有多个线程获取同步状态
void acquireSharedInterruptibly(int arg)//在acquireshared()方法的基础上增加了能响应中断的功能
boolean tryAcquireSharedNanos(int arg,long nanosTimeout)//在acquiresharedInterruptibly()方法的基础上增加了超时等待的功能boolean releaseShared(int arg)//共享式释放同步状态
要想深人掌握 AQS的底层实现,就需要对AQS提供的几个模板方法进行深入研究,如AOS 内部维护的同步队列是怎样的运行机制、AOS如何对同步状态进行管理等内容。
二、同步队列
当共享资源被某个线程占用时,其他请求该资源的线程将会被阻塞,从而进入同步队列,就数据结构而言,队列的实现方式大多是通过数组的形式,另外一种则是通过链表的形式。AQS中的同步队列就是通过链表的形式实现的。在学习的同时,大家可能会有这样的疑问:
(1)节点的数据结构是什么样的?
(2)队列是单向还是双向的?
(3)队列是带头节点还是不带头节点的?
下面我们通过源码一层层地分析这些问题,并从源码中找到答案。
1.Node节点
volatile int waitStatus//节点状态
volatile Node prev//前驱节点
volatile Node next;//后继节点
volatile Thread thread;//加入同步队列的线程引用
Node nextWaiter;//等待队列中的下一个节点
节点的状态如下:
int CANCELLED = -1; //节点从同步队列中取消
int SIGNAL = -1 //后继节点的线程处于等待状态,如果当前节点释放同步状态,会通知后继节点,是后继节点的线程能够运行
int CONDITION = -2; //当前节点进入等待队列中
int PROPAGATE = -3; //表示下一次共享式同步状态的锁将会无条件传播下去
int INITIAL = 0 //初始状态
2. 同步队列的结构
通过对节点属性的分析,可以看出每个节点都拥有前驱节点以及后继节点,显然同步队列是一个双向节点。下面执行一段 demo,通过 debug 观察同步队列的数据结构。
public class LockDemo {private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {for(int i = 0;i<5;i++){Thread thread = new Thread(() ->{lock.lock();try{Thread.sleep(10000);}catch(InterruptedException e){e.printStackTrace();}finally {lock.unlock();}});thread.start();}}}
上述示例代码中开启了5个线程,先获取锁再睡眠10s,这里让线程睡眠是想模拟当线程无法获取锁时进人同步队列的情况。当Thread-4(本例中的最后一个线程)获取锁失败后进入同步队列时,debug 运行时的状态如下图所示:
从上图中可以看出,Thread-0先获得锁后进行睡眠,其他线程(Thread-1、Thread-2Thread-3以及Thread-4)获取锁失败进入同步队列。同时还可以很清楚地看到,每个节点都有两个域:prev(前驱)和next(后继),并且每个节点还有获取同步状态失败的线程引用数据以及线程等待的状态信息。另外,AQS中有两个重要的成员变量:
private transient volatile Node head;
private transient volatile Node tail;
也就是说,AQS会通过头尾指针管理同步队列,同时实现对获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。
通过对源码走读的方式进行理解,我们对开篇提出的问题部分已经有了答案:
(1)AOS维护的同步队列中的数据节点,在源代码中通过静态内部类Node进行表达,Node 节点持有当前节点的同步状态以及前驱、后继节点。
(2)AQS维护的同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列。那么,
还剩下最后一个问题:AQS维护的同步队列中是否具有头节点(哨兵节点)?一般而言,为了解决队列入队出队操作时存在的边界问题,可能会通过构造头节点(即不含具体值的节点)统一边界问题带来的特殊性,以降低其复杂度。这个问题涉及队列的初始化操作,需要通过阅读线程获取资源失败后入队操作的源码才能找到答案。
三、 独占锁
1. 独占锁的获取(acquire()方法)
前面我们通过看源码和 debug的方式进一步了解了 AQS的底层原理,这里还是以上面的demo为例,调用lock()方法获取独占式锁。获取失败就将当前线程加入同步队列,成功则表示获取到锁资源,线程继续执行。而lock()方法实际上会调用AQS的acquire0方法,acquire()方法的源码如下:
public final void acquire(int arg){//先看同步状态是否获取成功,如果成功,则方法执行结束//如果失败,则会先调用addWaiter()方法,后调用acquireQueued()方法if(!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE) , args)){selfInterrupt();}}
在尝试获取锁资源时,首先通过tryAcquire()方法判断当前线程是否能够获取到资源,如果能够成功获取,则方法执行结束;如果不能成功获取,则会进行线程入队的操作。
当线程获取独占式锁失败后,就会将当前线程加入同步队列,那么加入队列的方式是怎么样的呢?接下来就应该研究一下addWaiter()和acquireQueued()方法。addWaiter()方法的源码如下:
private Node addWaiter(Node mode) {// 1. 将当前线程构建成 Node 类型Node node = new Node(Thread.currentThread(), mode);// 2. 当前尾节点是否为 nullNode pred = tail;if (pred != null) {// 2.2 将当前节点以尾插入的方式插入同步队列中node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 2.1. 当前同步队列尾节点为 null,说明当前线程是第一个加入同步队列进行等待的线程enq(node);return node;
}
程序的逻辑主要分为以下两部分。
(1)当前同步队列的尾节点为mul,说明当前节点可以进行人队操作,这里是通过调用enq()方法完成人队操作的。
(2)当前同步队列的尾节点不为null,则采用尾插人(compareAndSetTail()方法)的方式人队。另外,还会有一个问题,如果if(compareAndSetTail(pred,node))为false 怎么办?会继续执行 enq()方法,同时compareAndSetTail()方法是一个CAS操作。通常来说,如果 CAS操作失败,就会继续自旋进行重试。
经过分析后发现,enq()方法可能承担着两个任务:
1.在当前同步队列尾节点为nu1l时进行人队操作;
2.在队列初始化后完成节点的初次入队,以及CAS插人尾节点失败后继续自旋进行重试。
下面继续分析enq()方法的源码以寻找答案。enq()方法的源码如下:
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initialize// 1. 构造头节点if (compareAndSetHead(new Node()))tail = head;} else {// 2. 尾插入,CAS 操作失败后继续自旋进行重试node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
通过源码可以看出,当尾节点为null时,会首先创建头节点(哨兵节点)。带头节点与不带头节点相比,会在人队和出队的操作中获得更大的便捷性,因此同步队列选择了带头节点的链式存储结构,在这里也找到在开篇中提到的第3个问题的答案了。
那么带头节点的队列初始化时机是什么时候?自然是在tail为null 时,即当前线程第一次插入同步队列时。compareAndSetTail(t,node)方法会利用CAS操作设置尾节点,如果CAS操作失败,会在for(;;)循环中自旋不断进行重试,直至成功返回为止。因此,对enq()方法可以做这样的总结:
(1)如果当前线程节点是第一个插人同步队列的节点,通过调用compareAndSetHead(new Node())方法,可以完成同步队列的头节点的初始化;
(2)通过尾插人的方式完成节点首次入队操作,当CAS操作失败后,会通过自旋不断试CAS尾插人节点,直至成功为止。
至此,我们清楚了解了获取独占式锁失败的线程包装成节点后插入同步队列的过程。那紧接着会有下一个问题,在同步队列中的节点(线程)会做什么事情来保证自己能够获得独占式锁呢?带着这样的问题,我们就来看看 acquireQueued() 方法,从方法名就可以知道这个方法的作用是排队获取锁的过程,源码如下。
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// 1. 获得当前节点的先驱节点final Node p = node.predecessor();// 2. 当前节点能否获取独占式锁// 2.1 如果当前节点的先驱节点是头节点并且成功获取同步状态,即可获得独占式锁if (p == head && tryAcquire(arg)) {setHead(node);// 释放前驱节点p.next = null; // help GCfailed = false;return interrupted;}// 2.2 获取锁失败,线程进入等待状态等待获取独占式锁if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
具体的逻辑可以查看代码中的注释,整体来看这又是一个自旋的过程(for (;😉),代码首先获取当前节点的先驱节点,如果先驱节点是头节点并且成功获得同步状态
(if (p == head && tryAcquire(arg))),则当前节点所指向的线程就能够获取锁。反之,获取锁失败的话就会执行 shouldParkAfterFailedAcquire() 方法进入等待状态.
获取到锁资源出队操作的逻辑如下。
setHead(node);
//释放前驱节点
p.next=null;// help
failed = false;
return interrupted;
setHead()方法的源码如下:
private void setHead(Node node) {head = node;node.thread = null;node.prev = null;
}
将当前节点通过 setHead() 方法设置为队列的头节点,然后将之前头节点的 next 域设置为 null,pre 域设置为 null,即与队列断开,这时节点无任何引用,方便 GC 对内存进行回收.
那么,当获取锁失败时会调用 shouldParkAfterFailedAcquire() 方法和 parkAndCheckInterrupt() 方法。shouldParkAfterFailedAcquire() 方法的源码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node){int ws = pred.waitStatus;if (ws == Node.SIGNAL)return true;if(ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;}else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}
shouldParkAfterFailedAcquire() 方法主要是使用 compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 的 CAS 操作将节点状态由 INITIAL 设置为 SIGNAL。如果设置状态失败,则会继续用 acquireQueued() 方法通过 for(;😉 完成自旋操作,继续重试。直至 compareAndSetWaitStatus() 方法设置节点状态为 SIGNAL 时,shouldParkAfterFailedAcquire() 方法返回 true,才会执行 parkAndCheckInterrupt() 方法,该方法的源码如下:
private final boolean parkAndCheckInterrupt() {// 使该线程阻塞LockSupport.park(this);return Thread.interrupted();
}
该方法的关键是会调用 LockSupport.park() ,该方法是用来阻塞当前线程的。通过以上分析,acquireQueued() 方法在自旋过程中主要完成了以下两件事情:
(1)自旋过程中,如果当前节点的前驱节点是头节点,并且能够获得同步状态,该方法调用结束就退出,表示当前线程获取到同步资源;
(2)自旋过程中如果获取锁失败,先将节点状态设置为 SIGNAL,后调用 LockSupport.park() 方法使当前线程阻塞,然后等待被唤醒。
经过上面的分析,独占式锁的获取过程也就是 acquire() 方法的执行流程如下图所示:
2.独占锁的释放(release()方法)
在理解了锁获取的流程后,锁释放的流程相对而言就比较容易理解了,具体源代码如下:
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}
这段代码的逻辑就比较容易理解了,如果同步状态释放成功(tryRelease() 方法返回 true),才会执行 if 块中的代码,当 head 指向的头节点不为 null 并且该节点的状态值不为 0,才会执行 unparkSuccessor() 方法。unparkSuccessor() 方法的源码如下:
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;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;}// 后继节点不为 null,并且是在等待获取同步状态的节点if (s != null)LockSupport.unpark(s.thread);
}
源码的具体流程信息可以看注释,主要有两种场景:
1 .只有头节点(末尾的 if 判断不会生效),因此不存在是否唤醒后继节点的操作。
2. 队列中存在部分节点已经获取到同步状态了,那么就需要从后向前找到距离头节点最近的正在等待获取同步状态的节点,通过调用 LockSupport.unpark() 方法唤醒该节点的后继节点所引用的线程。
因此,每次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个 FIFO(先进先出)的过程。
这里通过对源码的分析,帮助大家了解了AQS同步队列入队以及出队的操作,这是 AQS 实现中最复杂的部分。只有我们在深入理解这些底层原理后,在以后的并发编程中才能够做到融会贯通。现在对整体流程做一个小总结:
(1)线程获取锁失败,线程被封装成 Node 进行入队操作,核心方法为 addWaiter() 和 enq(),同时 enq() 方法完成对同步队列的头节点初始化工作、节点首次入队操作以及 CAS 操作失败后的重试工作。
(2)线程获取锁是一个自旋的过程,当且仅当当前节点的前驱节点是头节点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁。不满足上述条件时,就会调用LockSupport.park() 方法使线程阻塞。
(3)释放锁时会通过 LockSupport.unpark() 方法唤醒后继节点。
整体来说,在获取同步状态时,AQS 维护一个同步队列,获取同步状态失败的线程会加入队列进行自旋。移除队列(或停止自旋)的条件是前驱节点为头节点并且成功获得了同步状态。在释放同步状态时,同步器会调用 unparkSuccessor() 方法唤醒后继节点。
3.可中断式获取锁(acquireInterruptibly() 方法)
Lock 相较于 synchronized 失去了隐式加锁和释放锁的便利性,但是 Lock 增加了响应中断以及超时等待的特性。这些 Lock 的特性是如何实现的呢?带着这样的疑问,我们继续通过阅读源代码的方式了解这些底层原理。
响应中断式锁通过调用 lock.lockInterruptibly() 方法,会在底层调用 AQS 的 acquireInterruptibly() 方法,源码如下:
public final void acquireInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (!tryAcquire(arg))// 线程获取锁失败doAcquireInterruptibly(arg);
}
在获取同步状态失败后,就会调用 doAcquireInterruptibly() 方法,源码如下:
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 GCfailed = false;return;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// 线程中断抛异常throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}
具体逻辑可以查看代码注释,在理解第一部分 acquire() 方法的原理后,再看这段代码就会觉得相当简单。它们的基本逻辑几乎是一致的,唯一的区别就是当 parkAndCheckInterrupt() 方法返回 true 时说明当前线程被中断了,同时会抛出被中断异常。
4.超时等待式获取锁(tryAcquireNanos()方法)
通过调用 lock.tryLock(timeout,TimeUnit) 方法达到超时等待获取锁的效果,该方法会在出现以下三种情况时返回:
(1)在超时时间内,当前线程成功获取了锁;
(2)当前线程在超时时间内被中断;
(3)超时时间结束,仍未获得锁返回 false。
该方法会调用 AQS 的 tryAcquireNanos() 方法,具体源码如下:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquire(arg) ||// 实现超时等待的效果doAcquireNanos(arg, nanosTimeout);
}
很显然,这段源码最终是靠 doAcquireNanos() 方法实现超时等待的效果,具体源码如下:
private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (nanosTimeout <= 0L)return false;// 1. 根据超时时间和当前时间计算出截止时间final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (; ; ) {final Node p = node.predecessor();// 2. 当前线程获得锁资源,进行出队操作if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return true;}// 3.1 重新计算超时时间nanosTimeout = deadline - System.nanoTime();// 3.2 已经超时返回 falseif (nanosTimeout <= 0L)return false;// 3.3 线程阻塞等待if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// 3.4 线程被中断,抛出异常if (Thread.interrupted())throw new InterruptedException();} finally{if (failed)cancelAcquire(node);}}}
程序逻辑如下图所示:
上述程序的实现逻辑和独占锁中可响应中断式获取锁的实现流程基本一致,唯一的不同在于获取锁失败后对超时时间的处理上。首先按照现在时间和超时时间计算出理论上的截止时间,如当前时间是 8h 10min,超时时间是 10min,那么根据 deadline = System.nanoTime() + nanosTimeout 计算出刚好达到超时时间的系统时间就是 8h 10min + 10min = 8h 20min。然后根据 deadline - System.nanoTime() 就可以判断出是否已经超时。例如,当前系统时间是 8h 30min,显而易见,已经超过了理论上的系统时间 8h 20min,deadline - System.nanoTime() 计算出来的结果就是一个负数,自然会在执行 3.2 步的 if 判断时返回 false。如果还没有超时,即执行 3.2 步的 if 判断时返回 true,就会继续执行 3.3 步调用 LockSupport.parkNanos() 方法使当前线程阻塞,同时在 3.4 步增加了对中断的检测,若检测出被中断,就直接抛出被中断异常。
四、共享锁
1. 共享锁的获取(acquireShared()方法)
与分析独占式锁的方式一样,本小节同样会对源码进行分析以掌握共享式锁的底层原理。共享锁的获取方法为 acquireShared(),源码如下:
public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);
}
这段源码的逻辑很容易理解,首先调用 tryAcquireShared() 方法,返回值是 int 类型,当返回值大于或等于 0 时方法结束,表明已成功获取锁,否则表明获取同步状态失败,即该节点所引用的线程获取锁失败,然后会继续向下执行 doAcquireShared() 方法,该方法的源码如下:
private void doAcquireShared(int arg) {final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {// 当该节点的前驱节点是头节点且成功获取同步状态setHeadAndPropagate(node, r);p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}
有了上述分析独占式锁的理论基础,对共享式锁的原理分析就是非常简单的一件事。其整体逻辑几乎和独占式锁的获取一模一样,只不过这里的自旋过程中能够退出的条件是当前节点的前驱节点是头节点并且 tryAcquireShared(arg) 方法的返回值大于或等于 0,才能成功获得同步状态。
2. 共享锁的释放(releaseShared()方法)
共享锁的释放在 AQS 中会调用 releaseShared() 方法,源码如下:
public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;
}
当成功释放同步状态之后,tryReleaseShared() 会继续执行 doReleaseShared() 方法:
private void doReleaseShared() {for (; ; ) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue; // loop to recheck casesunparkSuccessor(h);} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue; // loop on failed CAS}if (h == head) // loop if head changedbreak;}}
这段方法与独占式锁的释放过程有些许不同,在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全地释放同步状态,这里采用 CAS 保证,当 CAS 操作失败,执行 continue,在下一次循环中进行重试。
3.可中断式获取共享锁(acquireSharedInterruptibly()方法)
acquireSharedInterruptibly() 方法的源码如下:
public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);
}
如果当前线程被中断,会直接抛出被中断异常;如果没有发生中断,则会尝试获取到同步状态,如果获取到同步状态则方法正常结束。反之,没有获取到同步状态,则会执行 doAcquireSharedInterruptibly() 方法,源码如下:
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException {final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (; ; ) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();} finally{if (failed)cancelAcquire(node);}}}
该方法的逻辑和 doAcquireShared() 方法的逻辑基本一致。
两个方法的差异点在于:doAcquireShared() 方法不响应中断,当判断当前线程被中断后,会继续执行 selfInterrupt() 方法。但用 doAcquireSharedInterruptibly() 方法判断线程中断后,会抛出 InterruptedException。
4.超时等待式获取共享锁(tryAcquireSharedNanos()方法)
超时等待式获取共享锁与超时等待式获取独占锁的逻辑基本一致,只有一点差别,读者可以将两者进行对比以便于理解。tryAcquireSharedNanos() 方法的源码如下:
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquireShared(arg) >= 0 ||doAcquireSharedNanos(arg, nanosTimeout);
}
程序的逻辑很容易理解,当获取共享锁失败后,就会执行 doAcquireSharedNanos() 方法完成等待获取共享锁的过程,源码如下:
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException {if (nanosTimeout <= 0L)return false;final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return true;}}nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L)return false;if (shouldParkAfterFailedAcquire(p, node) &&nanosTimeout > spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);if (Thread.interrupted())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}
代码的整体逻辑与超时获取独占式锁的核心方法 doAcquireNanos() 的逻辑基本相同。
两者的差异仅仅体现在两点:
1.获取同步状态的方式不同,独占式锁是通过 tryAcquire() 方法,而共享式锁是通过 tryAcquireShared() 方法,这是由两个同步组件本身不同的同步语义导致的;
2.两者获取到同步状态后对头节点以及节点状态处理的方式不同。但这两点差异都是由两个同步组件本身所实现的同步语义不同导致的,对超时等待的特性的实现方式,包括对超时时间的判断和自旋等待的处理流程并没有太大的区别。
总结
本文主要介绍了AbstractQueuedSynchronizer(AQS)的主要模板方法,通过阅读源码的方式了解其运作具体细节。
以上就是本文全部内容,感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!