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、同步代码块

代码演示:

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、同步方法

代码演示:


/*** @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、代码展示:

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 类实现

// 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 类实现

// Makes available the permit for the given thread
public static void unpark(Thread thread) {if (thread != null)UNSAFE.unpark(thread);
}

*LockSupport代码示例

代码演示:

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()呢?

代码演示:

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的情况呐?

代码演示:

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只能等待,。进入候客区

演示代码:

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、一个就是如何对线程的进行处理

完结撒花!!!欢迎小伙伴们提出疑问,留言讨论!!!

我们下期见!

闲聊AQS面试和源码解读---可重入锁、LockSupport、CAS;从ReentrantLock源码来看公平锁与非公平锁、AQS到底是怎么用CLH队列来排队的?相关推荐

  1. Flink-SQL源码解读(一)window算子的创建的源码分析

    本文大体框架参考 https://blog.csdn.net/LS_ice/article/details/90711744 flink版本:1.9 Intro 作为无限流的核心机制,流可以分割为大小 ...

  2. JUC AQS ReentrantLock源码分析

    Java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还 ...

  3. Java并发——结合CountDownLatch源码、Semaphore源码及ReentrantLock源码来看AQS原理

    前言: 如果说J.U.C包下的核心是什么?那我想答案只有一个就是AQS.那么AQS是什么呢?接下来让我们一起揭开AQS的神秘面纱 AQS是什么? AQS是AbstractQueuedSynchroni ...

  4. 面试官系统精讲Java源码及大厂真题 - 32 ReentrantLock 源码解析

    32 ReentrantLock 源码解析 才能一旦让懒惰支配,它就一无可为. 引导语 上两小节我们学习了 AQS,本章我们就要来学习一下第一个 AQS 的实现类:ReentrantLock,看看其底 ...

  5. ReentrantLock源码分析

    ReentrantLock源码分析 前言 最近公司比较忙,整天忙着做项目.做需求,感觉整个人昏昏沉沉的,抬头看天空感觉都是灰色的~~,其实是杭州的天本来就是这个颜色,手动滑稽`~(^o^)/~`.废话 ...

  6. 多线程(三)之ReentrantLock源码解析

    2019独角兽企业重金招聘Python工程师标准>>> 今天分析ReentrantLock类的源码,在看源码之前,先学习AQS(AbstractQueuedSynchronizer) ...

  7. Java8 ReentrantLock 源码分析

    一.ReentrantLock 概述 1.1 ReentrantLock 简介 故名思义,ReentrantLock 意为可重入锁,那么什么是可重入锁呢?可重入意为一个持有锁的线程可以对资源重复加锁而 ...

  8. synchronized 和 reentrantlock 区别是什么_JUC源码系列之ReentrantLock源码解析

    目录 ReentrantLock 简介 ReentrantLock 使用示例 ReentrantLock 与 synchronized 的区别 ReentrantLock 实现原理 Reentrant ...

  9. 【java】java ReentrantLock 源码详解

    文章目录 1.概述 2.问题 3.ReentrantLock源码分析 3.1 类的继承关系 3.2 类的内部类 3.2.1 Sync类 3.2.2 NonfairSync类 3.2.3 FairSyn ...

最新文章

  1. HTML5培训教程:HTML5基础介绍
  2. 推出应用加速器 伟库网为用户应用体验上保险
  3. 微信小程序原生组件swiper在mpvue工程中使用注意事项
  4. java 多条件比较_Java 多条件复杂排序小结
  5. Python3 实现批量图像数据增强(扩增)并复制xml标签文件【目标检测笔记】
  6. head.s 剖析——Linux-0.11 剖析笔记(五)
  7. 【C语言】逗号运算符 ,
  8. iPhone在华智能机市场份额首次下滑
  9. php函数库快速记忆法_PHP速成大法
  10. 给UIWebView增加搜索栏
  11. rollup学习小记
  12. 搭建基于C#和 Appium 的 Android自动测试环境
  13. 组合枚举——妖梦拼木棒(洛谷 P3799)
  14. 文件——rstrip() 、lstrip()和 strip()、zip() 函数
  15. rhel源更换为centos源
  16. PHP 将二维数组转成一维数组
  17. 【Python】statsmodels.formula.api模块中ols参数的解释
  18. 传奇DBC数据库变量详细解释传奇DB文件详解
  19. 2019年TW的技术雷达
  20. CPU基础---设计一个8位的并行加法器

热门文章

  1. win10在命令行运行C\C++程序
  2. 第二十章 我国农村土地管理
  3. 机械动作时序图怎么画_程序员必备画图技能之——时序图
  4. HTML5新增选择器权重
  5. 计算机创新者印象(2):从罗伯茨到比尔.盖茨
  6. ebay账号防关联4要素(ebay防关联软件)
  7. iOS Swift处理点9图片
  8. Linux配DD虚拟带库,ubuntu安装虚拟磁带库mhvtl的方法
  9. 4W家庭理财 V2.6
  10. 苹果手机用计算机打不开怎么回事啊,苹果手机开机后一直显示苹果标志开不了机是怎么回事...