AQS原理可谓是JUC面试中的重灾区之一,今天我们就来一起看看AQS到底是什么?
这里我先整理了一些JUC面试最常问的问题?
1、Synchronized 相关问题以及可重入锁 ReentrantLock及其他显式锁相关问题
1、 Synchronized 用过吗,其原理是什么?
2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁
3、什么是可重入性,为什么说Synchronized 是可重入锁?
4、JVM对Java的原生锁做了哪些优化?
5、为什么说Synchronized是非公平锁?
6、什么是锁消除和锁粗化?
7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么优点和缺点?
8、乐观锁一定就是好的吗?
9、跟Synchronized相比,可重入锁ReentrantLock 其实现原理有什么不同?
10、那么请谈谈AQS框架是怎么回事儿?
11、请尽可能详尽地对比下Synchronized 和ReentrantLock的异同。
12、ReentrantLock 是如何实现可重入性的?
…
在正式开始AQS之前,我们需要先了解下课重入锁和LockSupport的原理。开始淦!
一、可重入锁
1、可重入锁的概念
可重入锁又名 递归锁, 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。从字面意思来看,可重入也就是可以再次进入同步锁。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。即自己可以获取自己的内部锁。
2、可重入锁的种类
隐式锁: Synchronized关键字使用的锁就是隐式锁,其默认是可重入锁。
1、同步代码块
代码演示:
java">package AQS.可重入锁.同步代码块;
/**
* @program: juc
* @description 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
* * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
* @author: 不会编程的派大星
* @create: 2021-08-14 15:29
**/
public class ReEnterLockDemo1 {
static Object ObjectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (ObjectLock){
System.out.println(Thread.currentThread().getName() +"---外层调用");
synchronized (ObjectLock){
System.out.println(Thread.currentThread().getName()+"---中层调用");
synchronized (ObjectLock){
System.out.println(Thread.currentThread().getName()+"---内层调用");
}
}
}
},"t1").start();
}
}
演示结果:
可以看到哈,在同一个线程内成功获取通一把锁。
2、同步方法
代码演示:
java">
/**
* @program: juc
* @description可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
* * 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
* @author: 不会编程的派大星
* @create: 2021-08-15 10:55
**/
public class ReEnterLockDemo2 {
public synchronized void m1(){
System.out.println("---外层---");
m2();
}
private synchronized void m2() {
System.out.println("---中层---");
m3();
}
private synchronized void m3() {
System.out.println("---内层---");
}
public static void main(String[] args) {
new ReEnterLockDemo2().m1();
}
}
演示结果:
3、Synchronized锁原理
1、 使用 javap -c xxx.class 指令反编译字节码文件,可以看到有一对配对出现的 monitorenter 和 monitorexit 指令,一个对应于加锁,一个对应于解锁
如下图所示:
2、 从字节码文件中,可以看到有两个monitorexit指令,为什么会多出来一个monitorexit指令呢?
如果同步代码块中出现exception或者error的情况,则会调用第二个monitorexit指令俩保证锁的释放。
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锋对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
显式锁(LOCK):
在显式锁中,也有ReentrantLock这样的可重入锁。
1、代码展示:
java">import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-08-16 15:51
**/
public class ReentrantLockDemo1 {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println("---外层---");
lock.lock();
try {
System.out.println("---内层---");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t1").start();
}
}
演示结果:
在同一个线程内,内部成功获取到通一把锁。**一定要注意!!!加锁几次就要解锁几次!**否则另外一个线程就拿不到该锁! 接下来我们再来看看AQS的另一大前置知识点—LockSupport!
二、LockSupport
1、LockSupport的概念?
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,可以将其看作是线程等待唤醒机制(wait/notify)的加强版。
2、三种可以让线程等待和唤醒的方法
1: 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
注:wait和notify方法必须要在同步块或者方法里面且成对出现使用,先wait后notify才OK。
2: 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
注:传统的synchronized和Lock实现等待唤醒通知的约束:线程先要获得并持有锁,必须在锁块(synchronized或lock)中,必须要先等待后唤醒,等待后线程才能够被唤醒。
3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
LockSupport 类使用了一种名为 permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和零,默认是零。
*LockSupport类主要方法
*阻塞
park()/park(Object blocker)
park() 方法的作用:阻塞当前线程/阻塞传入的具体线程
permit 默认是 0,所以一开始调用 park() 方法,当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1 时,park() 方法会被唤醒,然后会将 permit 再次设置为 0 并返回。
park() 方法通过 Unsafe 类实现
java">// Disables the current thread for thread scheduling purposes unless the permit is available.
public static void park() {
UNSAFE.park(false, 0L);
}
*唤醒
unpark(Thread thread)
unpark() 方法的作用:唤醒处于阻断状态的指定线程
调用 unpark(thread) 方法后,就会将 thread 线程的许可 permit 设置成 1(注意多次调用 unpark()方法,不会累加,permit 值还是 1),这会自动唤醒 thread 线程,即之前阻塞中的LockSupport.park()方法会立即返回。
unpark() 方法通过 Unsafe 类实现
java">// Makes available the permit for the given thread
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
*LockSupport代码示例
代码演示:
java">import java.util.concurrent.locks.LockSupport;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-08-17 14:52
**/
public class LockSupportDemo1 {
public static void main(String[] args) {
Thread a = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"---进来了---");
//线程A阻塞
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"---我被唤醒了---");
},"A");
a.start();
new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName()+"唤醒,通知A");
},"B").start();
}
}
演示结果:
A线程先执行park()方法将通行证permit设置为0,这里其实无影响,因为permit初始值本来就为0,然后B线程呢在执行unpark(a)方法再将permit设置为1,这个时候呢,A线程就被唤醒了,就正常执行。
在上面哈,我们说过synchronized和lock都需要先等待在唤醒,否则线程就会一直处于等待状态,那在LockSupport中要是先unpark()再park()呢?
代码演示:
java">import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-08-17 15:41
**/
public class LockSupportDemo2 {
public static void main(String[] args) {
Thread a = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---进来了---");
//线程A阻塞
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"---我被唤醒了---");
});
a.start();
new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName()+"唤醒,通知A");
},"B").start();
}
}
演示结果:
因为有太通行证的存在,所以先执行unpark()方法并没有什么影响,因为permit默认为0,执行unpark()后permit为1,执行park()时,park()就可以光明正大的消费掉这个1,所以不会造成A线程阻塞。
在上面我们也说到了一个问题,那就是permit的上限为1,我们来想想假如没有考虑到permit上限值为1的情况呐?
代码演示:
java">import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-08-17 14:52
**/
public class LockSupportDemo3 {
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread a = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"---进来了---");
//线程A阻塞
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName()+"---我被唤醒了---");
},"A");
a.start();
new Thread(() -> {
LockSupport.unpark(a);
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName()+"唤醒,通知A");
},"B").start();
}
}
演示结果:
可以看到哈,即使有两次unpark(),但是permit的上限为1,A线程两个park()只能消费一次,还有一个park()就会造成A线程阻塞。
*LockSupport小的总结
以前的等待唤醒通知机制必须synchronized里面执行wait和notify,在lock里面执行await和signal,这上面这两个都必须要持有锁才能正常使用。
LockSupport:俗称锁中断,LockSupport 解决了 synchronized 和 lock 的痛点, 1、 LockSupport不用持有锁块,不用加锁,程序性能好,无须注意唤醒和阻塞的先后顺序,不容易导致卡死。LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。
2、 如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
3、 每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。
4、 当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
三、AbstractQueuedSynchronizer — AQS
接下来,终于进入到主题AQS,AQS,即抽象的队列同步器。
1、AQS介绍以及概念
一般我们说的 AQS 就是 java.util.concurrent.locks 包下的AbstractQueuedSynchronizer
;AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态;CLH:Craig、Landin and Hagersten 队列,是一个双向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO,如下图所示:
AQS继承关系:
2、AQS—JUC的基石
我们来看看一张图片,就知道为什么AQS是JUC的基石了
我们在JUC见到的很多类都是基于AQS搭建的。例如ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore等等许多。
通过这些关系,我们也可以更好的理解锁和同步器的关系:
锁, 面向锁的使用者。定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可,可以理解为用户层面的 API。
同步器, 面向锁的实现者。比如Java并发的开发者,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,Java 中有那么多的锁,就能简化锁的实现啦。
3、AQS能干嘛?
看下面这段话最好是结合着下面的图一起来理解和认识0.0!
加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理;
抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定 会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
再从另一个角度来看看,有阻塞就需要排队,实现排队必然需要队列:
1、 AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
2、 Node 节点是啥?答:你有见过 HashMap 的 Node 节点吗?JDK 用 static class Node<K,V> implements Map.Entry<K,V> { 来封装我们传入的 KV 键值对。这里也是一样的道理,JDK 使用 Node 来封装(管理)Thread
3、 可以将 Node 和 Thread 类比于候客区的椅子和等待用餐的顾客
我们来看看几个重要的成员变量
AQS的int变量state
AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态:零就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着去。
AQS的CLH队列
为一个双向队列,类似于银行侯客区的等待顾客,通过自旋等待,state变量判断其是否阻塞,从尾部入队从头部出队
AQS内部类Node
Node的等待状态waitState成员变量,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node,我们来看看内部结构
小总结
有阻塞就需要排队,实现排队必然需要队列,通过state 变量 + CLH双端 Node 队列实现,cas来控制变量state。
4、AQS到底是怎么排队的呢
排队的话就是使用我们上面所讲到的LockSupport.park()方法。
四、从ReentrantLock源码来剖析AQS、公平锁与非公平锁
1、ReentrantLock的原理
ReentrantLock 实现了 Lock 接口,在 ReentrantLock 内部聚合了一个 AbstractQueuedSynchronizer 的实现类,如下图所示:
2、公平锁和非公平锁
接下来就让我们通过ReentrantLock的源码来分析下公平锁与非公平锁。
在 ReentrantLock 内定义了静态内部类,分别为 NoFairSync(非公平锁)和 FairSync(公平锁)
ReentrantLock 的构造函数: 不传参数表示创建非公平锁;参数为 true 表示创建公平锁;参数为 false 表示创建非公平锁
我们来大概看一下lock()方法的执行过程
1
2
3
4
非公平锁:
公平锁:
在 ReentrantLock 中,NoFairSync 和 FairSync 中 tryAcquire() 方法的区别,可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors():是公平锁加锁时判断等待队列中是否存在有效节点的方法,如下图所示:
小总结
对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队
主要导致的差异:
1、公平锁: 公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
2、非公平锁: 不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一 个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
3、从非公平锁的lock()方法源码入手
演示案例说明: 假设 A、B、C 三个人都要去银行窗口办理业务,但是银行窗口只有一个个,带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制, 3个线程模拟3个来银行网点,受理窗口办理业务的顾客, A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理;第二个顾客,第二个线程,由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待, 进入候客区;第三个顾客,第三个线程,由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,。进入候客区
演示代码:
java">import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @program: juc
* @description
* @author: 不会编程的派大星
* @create: 2021-08-17 16:58
**/
public class AQSDemo1 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println("A该去窗口办理业务了");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
},"A").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("B该去窗口办理业务了");
} finally {
lock.unlock();
}
},"B").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("C该去窗口办理业务了");
} finally {
lock.unlock();
}
},"C").start();
}
}
我们来分析下这段代码在业务中的执行过程:
首先这里我们在创建锁时,是使用的默认构造函数,即创建的是非公平锁。
在开始之前 我们再来回忆下之前学的CAS原理,通过 Unsafe 提供的 compareAndSwapXxx() 方法保证修改操作的原子性(通过 CPU 原语保证),如果变量的值等于期望值,则修改变量的值为 update,并返回 true;若不等,则返回 false。this 代表当前对象,stateOffset 表示 state 变量在该对象中的偏移量
详情可以参考之前写过的一片文章
准备好了吗?带你读底层!深入理解CAS ,从juc原子类一路追溯到unsafe类再到ABA原子引用!
第一次执行lock()方法
第一次执行lock()方法时,state的变量值为0,这是就说明该lock锁还没有被占用,此时执行cas(0,1)判断,成功后,将state的值改为1
这里setExclusiveOwnerThread() 方法是将拥有该lock锁的线程修改为线程A
第二次执行lock()方法–线程B
由于第二次执行 lock() 方法,state 变量的值等于 1,表示 lock 锁已经被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state != expected,因此 CAS 失败,进入 acquire() 方法
接下俩我们再来分析下acquire() 方法下的几个方法
tryAcquire(arg) 方法的执行流程
子类中具体的实现:
这里线程B执行,getState()后state为1,说明lock锁已经被占用,(C==0这里的代码块大家应该都明白,cas,和上面一样),进入到下一个判断条件else if (current == getExclusiveOwnerThread()) ,这里current是当前线程,而getExclusiveOwnerThread是lock所占用的线程(排他锁,exclusive),所以不满足条件,最后返回false,表示没有抢占到lock锁。
这里在补充两个可能遇到的特殊情况:
1、 第一种情况是,走到 int c = getState() 语句时,此时线程 A 恰好执行完成,让出了 lock 锁,那么 state 变量的值为 0,当然发生这种情况的概率很小,那么线程 B 执行 CAS 操作成功后,将占用 lock 锁的线程修改为自己,然后返回 true,也就是第一个判断条件,表示抢占锁成功。其实这里还有一种情况,到下面 unlock() 方法我们在细谈。
2、 第二种情况为可重入锁的表现,假设 A 线程又再次抢占 lock 锁(当然是上面的示例代码里面并没有体现出来,但其他地方可能会遇到),这时 current == getExclusiveOwnerThread() 条件成立,将 state 变量的值加上 acquire,这种情况下也应该 return true,表示线程 A 正在占用 lock 锁。因此,state 变量的值是可以大于 1 的,也就是第二个判断条件
好!我们继续往下走
在 tryAcquire() 方法返回 false 之后,进行 ! 操作后为 true,那么会继续执行 addWaiter() 方法
ddWaiter(Node.EXCLUSIVE)方法
我们在之前就说过哈,Node 节点用于封装用户线程,这里将当前正在执行的线程通过 Node 封装起来(当前线程正是抢占 lock 锁没有抢占到的线程)来看看源码
在构建完node后,判断tail尾指针是否为空,双端队列此时肯定还没元素,那么执行enq(node) 方法,将封装了线程B的Node节点入队。
enq(node) 方法:构建双端同步队列
开始看enq源码前,我们先来说说双端队列,双端队列的第一个节点被称为虚节点(哨兵节点),其实并不存储任何信息,也就是占着茅坑不拉屎,真正第一个有数据的节点,是从第二个开始的,好,接下来让我们来看看enq(node) 方法:
我们来一起分析下这个过程:
第一次for循环,这里相当于一个指针指向尾指针,此时肯定为空,然后就创建一个哨兵节点,此时队列就只有这一个哨兵节点,既是头结点,也是尾结点。如下图所示:
在来看看第二次for循环,现在也就是将封装了B线程的节点node放入到双端队列中,这里尾结点并不是为空,所以进入到else分支中,这里用到的是尾插法,先将封装了B线程的node的prev前缀指向之前的tail,再利用CAS将新的node节点设置为新的尾结点,再将t的next后缀指向node,最后返回t所指向的节点结束循环,如下图所示:
这里补充一下设置尾结点的方法实现,也就是cas的应用:
最后呀,注意一点,哨兵节点和waitStatus均为0,表示正在阻塞队列中。
再来看看C线程的执行的执行过程,和B线程整体相差不大,有一丢丢区别,因为tail尾结点已经不为空了,所以在addWaiter() 方法中就已经将封装了C线程的node节点添加到队尾了,就不需要在执行enq()方法了。
执行完addWaiter() 方法后,下一步就是acquireQueued() 方法了。
acquireQueued() 方法的执行逻辑
我们来看看acquireQueued() 方法的源码,两个if判断都是放在for循环当中的。其实这个就相当于自旋锁,实现了自旋的操作
我们就以B线程为例,看看它执行完addWaiter()方法之后是怎么执行acquireQueued() 放的。首先,传入的参数为封装了B线程的node节点,我们也都知道,Node(B线程)的前驱节点为哨兵节点,所以p指向的就是哨兵节点,在第一个if条件中,p == head条件是满足的,但是tryAcquire(arg)方法抢占lock锁还是会返回false,所以会执行下面的shouldParkAfterFailedAcquire(p, node)方法,我们来看看其源码:
哨兵节点的waitStatus == 0,因此CAS操作会将哨兵节点的waitStatus改为Node.SIGNAL,其值为-1.
这里说一个注意点:compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 调用 unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); 实现,虽然 compareAndSwapInt() 方法内无自旋,但是在 acquireQueued() 方法中的 for( ; ; ) 能保证此自选操作成功
上诉方法完成后,哨兵节点的waitStatus就为-1了,如下图所示:
同时,该if判断条件下,还会执行一个parkAndCheckInterrupt() 方法
线程B之类就调用LockSupport.park()方法后就阻塞了,该线程就不会继续向下执行,程序就在这儿排队等待。
线程C和线程B类似,最后也会执行到LockSupport.park()这里就阻塞,然后进入等待区。
小总结
如果前驱节点的 waitstatus 是 SIGNAL 状态(-1),即 shouldParkAfterFailedAcquire() 方法会返回 true,程序会继续向下执行 parkAndCheckInterrupt() 方法,用于将当前线程挂起
根据 park() 方法 API 描述,程序在下面三种情况会继续向下执行:
1、 被 unpark
2、 被中断(interrupt)
3、 其他不合逻辑的返回才会然续向下执行
unlock方法来了
我们来看看线程A在unlock()时都发生了什么
tryRelease(arg) 方法的执行逻辑
线程 A 只加锁过一次,因此 state 的值为 1,参数 release 的值也为 1,因此 c == 0。将 free 设置为 true,表示当前 lock 锁已被释放,将排他锁占有的线程设置为 null,表示没有任何线程占用 lock 锁,再来看看unparkSuccessor(h) 方法
unparkSuccessor(h) 方法的执行逻辑
在 release() 方法中获取到的头结点 h 为哨兵节点,h.waitStatus == -1,所以waitStatus的值为-1,因此执行 CAS操作将哨兵节点的 waitStatus 设置为 0,并获取哨兵节点的下一个节点Node(线程B),并且LockSupport.unpark()唤醒线程B.
此时线程B在再回到lock的执行流程中来,也就是最后执行LockSupport.park()方法
回到上一层acquireQueued()方法中,此时 lock 锁未被占用,线程 B 执行 tryAcquire(arg) 方法能够抢到 lock 锁,并且将 state 变量的值设置为 1,表示该 lock 锁已经被占用
setHead()方法
传入的节点为 Node(线程B),头指针指向 nodeB 节点;将 nodeB 中封装的线程置为 null(因为已经获得锁了);nodeB 不再指向其前驱节点(哨兵节点)。这一切都是为了将 nodeB 作为新的哨兵节点,如下图所示:
将 p.next 设置为 null,这是原来的哨兵节点就是完全孤立的一个节点,此时 nodeB 作为新的哨兵节点
到这里,相信大家都看湿了吧,爽的通透了吧,终于淦完了!!!
最后我们再来一个小总结、
总结
AQS利用CAS原子操作维护自身的状态,结合LockSupport对线程进行阻塞和唤醒从而实现更为灵活的同步操作。
AQS主要的脉络就是:1、通过CAS操作维护自身的状态 2、一个就是如何对线程的进行处理
完结撒花!!!欢迎小伙伴们提出疑问,留言讨论!!!
我们下期见!