JUC-多线程(12. AQS)学习笔记

news/2024/12/22 18:40:07/

文章目录

  • 1. 可重入锁
    • 1.1. 概述
    • 1.2. 可重入锁类型
    • 1.3. Synchronized 可重入实现机理
  • 2. LockSupport
    • 2.1. LockSupport 是什么
    • 2.2. 3种线程等待唤醒的方法
      • 2.2.1 Object 的等待与唤醒
      • 2.2.2. Condition接口中的等待与唤醒
      • 2.2.3. 传统的 synchronized 和 Lock 实现等待唤醒通知的约束
    • 2.3. LockSupport 类
      • 2.3.1 阻塞
      • 2.3.2 唤醒
      • 2.3.3 LockSupport 代码示例
      • 2.3.4 LockSupport 重点说明
    • 2.4 LockSupport 面试题
      • 2.4.1 为什么可以先唤醒线程后阻塞线程?
      • 2.4.2 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
  • 3. AQS
    • 3.1 AQS 前置知识
    • 3.2 AQS 是什么?
    • 3.3 AQS 是 JUC 的基石
    • 3.4 AQS 能干嘛
    • 3.5 AQS 初步认识
      • 3.5.1 AQS初识
      • 3.5.2 AQS内部体系架构
      • 3.5.3. AQS同步队列的基本结构
      • 3.5.4 AQS底层是怎么排队的?
    • 3.6 从 ReentrantLock 开始解读 AQS
      • 3.6.1 ReentrantLock 实现关系
      • 3.6.2 公平锁 & 非公平锁
    • 3.7 AQS 源码解读
      • 3.7.1 从非公平锁的 lock() 入手
      • 3.7.2 acquire()
      • 3.7.3 tryAcquire(arg) :尝试抢占 arg 个线程
      • 3.7.4 addWaiter(Node.EXCLUSIVE) :进入等候区
      • 3.7.5 acquireQueued(node, arg)
      • 3.7.6 unlock()
    • 3.7、AQS 总结
      • 3.7.1 AQS 的考点
      • 3.7.2 AQS 源码解读案例图示

1. 可重入锁

1.1. 概述

  • 可重入锁,又叫递归锁
  • 同一个线程,在外层方法获取锁的时候,再进入该线程内层方法会自动获取锁(前提,锁对象必须是同一个对象),不会因为之前获取还没释放而阻塞。
  • Java 中的 ReentrantLock 和 Synchronized 都是可重入锁
  • 可重入锁的优点就是避免死锁

1.2. 可重入锁类型

  1. 隐式锁,Synchronized 关键字使用的锁,默认是可重入锁,由 JVM 层面自动控制加解锁
  2. 显式锁,即 Lock

1.3. Synchronized 可重入实现机理

  • 每一个锁对象都有一个锁计数器和一个指向持有该线程的指针
  • 当执行 monitorenter 时
    • 若目标锁对象的计数器为0,那么说明没有其他线程持有它,那么 Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器 +1
    • 若目标对象的计数器不为0
      • 若该锁的持有线程是当前线程,则将计数器 +1
      • 否则等待,直至持有该锁的线程将其释放,也即直至计数器为0
  • 当执行 monitorexit 时,虚拟机会将该锁对象的计数器 -1,计数器为0时表示释放该锁

2. LockSupport

2.1. LockSupport 是什么

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,可以将其看作是线程等待唤醒机制(wait/notify)的加强版

2.2. 3种线程等待唤醒的方法

  1. 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
  2. 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  3. LockSupport类可以阻塞 park() 当前线程以及唤醒 unpark() 指定被阻塞的线程

2.2.1 Object 的等待与唤醒

  1. 正常的使用情况,及其结果

    static Object objectLock = new Object();private static void synchronizedWaitNotify() {new Thread(() -> {synchronized (objectLock) {System.out.println(Thread.currentThread().getName() + "\t" + "------come in");try {objectLock.wait(); // 等待} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");}}, "A").start();new Thread(() -> {synchronized (objectLock) {objectLock.notify(); // 唤醒System.out.println(Thread.currentThread().getName() + "\t" + "------通知");}}, "B").start();
    }
    
    • 程序运行结果:A 线程先执行,执行 objectLock.wait() 后被阻塞,B 线程在 A 线程之后执行 objectLock.notify() 将 A线程唤醒

    在这里插入图片描述

  2. 异常情况 1 : wait() 和 notify() 方法 不在 Synchronized 内使用

    static Object objectLock = new Object();private static void synchronizedWaitNotify() {new Thread(() -> {//synchronized (objectLock) {System.out.println(Thread.currentThread().getName() + "\t" + "------come in");try {objectLock.wait(); // 等待} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");//}}, "A").start();new Thread(() -> {//synchronized (objectLock) {objectLock.notify(); // 唤醒System.out.println(Thread.currentThread().getName() + "\t" + "------通知");//}}, "B").start();
    }
    
    • 不在 synchronized 关键字中使用 wait() 和 notify() 方法 ,将抛出java.lang.IllegalMonitorStateException 异常

    在这里插入图片描述

  3. 异常情况 2:先 notify() 后 wait()

    static Object objectLock = new Object();private static void synchronizedWaitNotify() {new Thread(() -> {try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}synchronized (objectLock) {System.out.println(Thread.currentThread().getName() + "\t" + "------come in");try {objectLock.wait(); // 等待} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");}}, "A").start();new Thread(() -> {synchronized (objectLock) {objectLock.notify(); // 唤醒System.out.println(Thread.currentThread().getName() + "\t" + "------通知");}}, "B").start();
    }
    
    • B 线程先执行 objectLock.notify(),A 线程再执行 objectLock.wait(),这样 A 线程无法被唤醒

    在这里插入图片描述

  4. 小总结

    • wait和notify方法必须要在同步块或者方法里面且成对出现使用
    • 先wait后notify才OK

2.2.2. Condition接口中的等待与唤醒

  1. 正常情况下

    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();private static void lockAwaitSignal() {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t" + "------come in");try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");} finally {lock.unlock();}}, "A").start();new Thread(() -> {lock.lock();try {condition.signal();System.out.println(Thread.currentThread().getName() + "\t" + "------通知");} finally {lock.unlock();}}, "B").start();
    }
    
    • A 线程先执行,执行 condition.await() 后被阻塞,B 线程在 A 线程之后执行 condition.signal() 将 A线程唤醒

    在这里插入图片描述

  2. 异常情况1:不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法

    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();private static void lockAwaitSignal() {new Thread(() -> {//lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t" + "------come in");try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");} finally {//lock.unlock();}}, "A").start();new Thread(() -> {//lock.lock();try {condition.signal();System.out.println(Thread.currentThread().getName() + "\t" + "------通知");} finally {//lock.unlock();}}, "B").start();
    }
    
    • 不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法,将抛出 java.lang.IllegalMonitorStateException 异常
      在这里插入图片描述
  3. 异常情况2:先 signal() 后 await()

    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();private static void lockAwaitSignal() {new Thread(() -> {try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t" + "------come in");try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");} finally {lock.unlock();}}, "A").start();new Thread(() -> {lock.lock();try {condition.signal();System.out.println(Thread.currentThread().getName() + "\t" + "------通知");} finally {lock.unlock();}}, "B").start();
    }
    
    • B 线程先执行 condition.signal(),A 线程再执行 condition.await(),这样 A 线程无法被唤醒

    在这里插入图片描述

2.2.3. 传统的 synchronized 和 Lock 实现等待唤醒通知的约束

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中

  • 必须要先等待后唤醒,线程才能够被唤醒

2.3. LockSupport 类

  • LockSupport 类使用了一种名为 permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和 0,默认是 0。
  • 可以把许可看成是一种(0, 1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是 1。

2.3.1 阻塞

  • 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);
    }
    

2.3.2 唤醒

  • 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);
    }
    

2.3.3 LockSupport 代码示例

  1. 正常使用情况

    private static void lockSupportParkUnpark() {Thread a = new Thread(() -> {System.out.println(Thread.currentThread().getName() + "\t" + "------come in");LockSupport.park(); // 线程 A 阻塞System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒");}, "A");a.start();new Thread(() -> {LockSupport.unpark(a); // B 线程唤醒线程 ASystem.out.println(Thread.currentThread().getName() + "\t" + "------通知");}, "B").start();
    }
    
    • A 线程先执行 LockSupport.park() 方法将通行证(permit)设置为 0,其实这并没有什么鸟用,因为 permit 初始值本来就为 0,然后 B 线程执行 LockSupport.unpark(a) 方法将 permit 设置为 1,此时 A 线程可以通行

    在这里插入图片描述

  2. 异常情况:没有考虑到 permit 上限值为 1

    private static void lockSupportParkUnpark() {Thread a = new Thread(() -> {try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis());LockSupport.park();LockSupport.park();System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis());}, "A");a.start();new Thread(() -> {LockSupport.unpark(a);LockSupport.unpark(a);System.out.println(Thread.currentThread().getName() + "\t" + "------通知");}, "B").start();
    }
    
    • 由于 permit 的上限值为 1,所以执行两次 LockSupport.park() 操作将导致 A 线程阻塞

    在这里插入图片描述

  3. LockSupport 小总结

    • LockSupport:俗称锁中断,LockSupport 解决了 synchronized 和 lock 的痛点
    • LockSupport 不需要 Synchronized 或者 Lock ,不用持有锁块,不用加锁,程序性能好
    • 无须注意唤醒和阻塞的先后顺序,不容易导致卡死

2.3.4 LockSupport 重点说明

  1. LockSupport是用来创建锁和其他同步类的基本线程阻塞原语

    • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码
  2. LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程

    • LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。

    • 如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。

    • 每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。

  3. 形象的理解

    • 线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
    1. 当调用park方法时
      • 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
      • 如果无凭证,就必须阻塞等待凭证可用;
    2. 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

2.4 LockSupport 面试题

2.4.1 为什么可以先唤醒线程后阻塞线程?

  • 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

2.4.2 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

  • 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

3. AQS

3.1 AQS 前置知识

公平锁和非公平锁
可重入锁
LockSupport
自旋锁
数据结构之链表
设计模式之模板设计模式

3.2 AQS 是什么?

  1. 字面意思

    • AQS(AbstractQueuedSynchronizer):抽象的队列同步器
    • 一般我们说的 AQS 指的是 java.util.concurrent.locks 包下的 AbstractQueuedSynchronizer,但其实还有另外三种抽象队列同步器:AbstractOwnableSynchronizer、AbstractQueuedLongSynchronizer 和 AbstractQueuedSynchronizer
  2. 技术翻译

    • AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态
    • CLH:是一个双向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO (由 Craig、Landin and Hagersten 这三个大牛名字组成)
      在这里插入图片描述

3.3 AQS 是 JUC 的基石

  • 常见示例

    ReentrantLock
    CountDownLatch
    ReentrantReadWriteLock
    Semaphore
    ……

  • 进一步理解锁和同步器的关系

    • 锁,面向锁的使用者。定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可,可以理解为用户层面的 API。
    • 同步器,面向锁的实现者。比如Java并发大神Douglee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,Java 中有那么多的锁,就能简化锁的实现啦。

3.4 AQS 能干嘛

  • AQS:加锁会导致阻塞

    有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理

    抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

    既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。

    在这里插入图片描述

3.5 AQS 初步认识

3.5.1 AQS初识

  • 为实现阻塞锁和相关的同步器提供一个框架,它是依赖于先进先出的一个等待

  • 依靠单个原子int值来表示状态,通过占用和释放方法,通过改变状态值来获得锁

  • 有阻塞就需要排队,实现排队必然需要队列

    • AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
    • Node 节点是啥?答:你有见过 HashMap 的 Node 节点吗?JDK 用 static class Node<K,V> implements Map.Entry<K,V> { 来封装我们传入的 KV 键值对。这里也是一样的道理,JDK 使用 Node 来封装(管理)Thread
    • 可以将 Node 和 Thread 类比于候客区的椅子和等待用餐的顾客

3.5.2 AQS内部体系架构

  1. AQS的int变量 —— state
    AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态:零就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着去

  2. AQS的CLH队列
    CLH队列,为一个双向队列,类似于银行侯客区的等待顾客

  3. 内部类Node(Node类在AQS类内部)
    Node的等待状态waitState成员变量,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node

    • Node类的内部结构
      static final class Node{//共享static final Node SHARED = new Node();//独占static final Node EXCLUSIVE = null;//线程被取消了static final int CANCELLED = 1;//后继线程需要唤醒static final int SIGNAL = -1;//等待condition唤醒static final int CONDITION = -2;//共享式同步状态获取将会无条件地传播下去static final int PROPAGATE = -3;// 初始为0,其他几种状态就是上面的,CANCELLED 、SIGNAL 、CONDITION 、PROPAGATE volatile int waitStatus;// 前置节点volatile Node prev;// 后继节点volatile Node next;// ...
      
  4. 总结

    • 有阻塞就需要排队,实现排队必然需要队列,通过state 变量 + CLH双端 Node 队列实现

3.5.3. AQS同步队列的基本结构

在这里插入图片描述

3.5.4 AQS底层是怎么排队的?

通过调用 LockSupport.pork() 来进行排队

3.6 从 ReentrantLock 开始解读 AQS

3.6.1 ReentrantLock 实现关系

  • ReentrantLock 类是 Lock 接口的实现类,基本都是通过【聚合】了一个【队列同步器 Sync】的子类完成线程访问控制的
  • Sync 类又继承了 AQS
  • 公平锁、非公平锁也是基于 Sync
    在这里插入图片描述

3.6.2 公平锁 & 非公平锁

  • 在 ReentrantLock 内定义了静态内部类,分别为 NoFairSync(非公平锁)和 FairSync(公平锁)
    -
  • ReentrantLock 的构造函数:不传参数表示创建非公平锁
  • 参数为 true 表示创建公平锁;参数为 false 表示创建非公平锁
    在这里插入图片描述

公平与非公平锁,是如何获得锁的

  • lock() 方法的执行流程:以 NonfairSync 为例
    在这里插入图片描述

  • 公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件: hasQueuedPredecessors()

  • hasQueuedPredecessors() 方法是公平锁加锁时判断等待队列中是否存在有效节点的方法

    在这里插入图片描述

公平锁与非公平锁的总结

  • 对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

    1. 公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
    2. 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一 个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

    在这里插入图片描述

  • 而 acquire() 方法最终都会调用 tryAcquire() 方法
    在这里插入图片描述

  • 在 NonfairSync 和 FairSync 中均重写了其父类 AbstractQueuedSynchronizer 中的 tryAcquire() 方法
    在这里插入图片描述

3.7 AQS 源码解读

  • 整个 ReentrantLock 的加锁过程,大致分为三个阶段:
    1. 尝试加锁
    2. 加锁失败,线程入队列
    3. 线程入队列后,进入阻塞状态

先从示例代码入手

  • 源码解读比较困难,我们这里举个栗子,假设 A、B、C 三个人都要去银行窗口办理业务,但是银行窗口只有一个个,我们使用 lock.lock() 模拟这种情况

    public class AQSDemo {public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();// 带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制// 3个线程模拟3个来银行网点,受理窗口办理业务的顾客// A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理new Thread(() -> {lock.lock();try {System.out.println("-----A thread come in");try {TimeUnit.MINUTES.sleep(20);} catch (Exception e) {e.printStackTrace();}} finally {lock.unlock();}}, "A").start();// 第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,// 进入候客区new Thread(() -> {lock.lock();try {System.out.println("-----B thread come in");} finally {lock.unlock();}}, "B").start();// 第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,// 进入候客区new Thread(() -> {lock.lock();try {System.out.println("-----C thread come in");} finally {lock.unlock();}}, "C").start();}
    }
    

3.7.1 从非公平锁的 lock() 入手

  • 之前已经讲到过,new ReentrantLock() 不传参默认是非公平锁,调用 lock.lock() 方法最终都会执行 NonfairSync 重写后的 lock() 方法

先来看看线程 A(客户 A)的执行流程

  • 第一次执行 lock() 方法
    在这里插入图片描述

  • 由于第一次执行 lock() 方法,state 变量的值等于 0,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state == expected == 0,因此 CAS 成功,将 state 的值修改为 1
    在这里插入图片描述

  • setExclusiveOwnerThread() :将拥有 lock 锁的线程修改为线程 A
    在这里插入图片描述

再来看看线程 B(客户 B)的执行流程
第二次执行 lock() 方法

  • 由于第二次执行 lock() 方法,state 变量的值等于 1,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state != expected,因此 CAS 失败,进入 acquire() 方法
    在这里插入图片描述
  • 之后进入队列同步器进行排队

3.7.2 acquire()

  • 通过上面所述,当线程 B 发现锁已经被占用,那么走进了 acquire 方法
    在这里插入图片描述

3.7.3 tryAcquire(arg) :尝试抢占 arg 个线程

  • 在 tryAcquire 中没有其他内容,直接抛出了异常,这就是设计模式中的 模板设计模式,也即所有 AQS 的子类必须实现该方法,否则就抛异常
    在这里插入图片描述

  • 查看所有实现类
    在这里插入图片描述

  • 这里以 ReentrantLock 的非公平锁 NonfairSync 为例,在 tryAcquire() 方法中调用了 nonfairTryAcquire() 方法,注意,这里传入的参数都是 1
    在这里插入图片描述

  • nonfairTryAcquire(acquires) 正常的执行流程:
    在这里插入图片描述

  • 在 tryAcquire() 方法返回 false 之后,取反后为 true,那么会继续执行 addWaiter() 方法
    在这里插入图片描述

3.7.4 addWaiter(Node.EXCLUSIVE) :进入等候区

  • 之前讲过,Node 节点用于封装用户线程,这里 Node.EXCLUSIVE 就是之前说的 Node 对象中的属性,排他
    在这里插入图片描述

  • 注意:哨兵节点和 nodeB 节点的 waitStatus 均为 0,表示在等待队列中

  • 上图文字的示意图

  • 第一次执行 for 循环:当线程 B 进来时,双端同步队列为空,此时肯定要先构建一个哨兵节点。并头指针指向哨兵节点,尾指针也指向该哨兵节点
    在这里插入图片描述

  • 第二次执行 for 循环:将装着线程 B 的节点 (NodeB) 放入双端同步队列中。以尾插法的方式,先将 NodeB 设为尾节点,再将 NodeB 的前指针指向 NodeNew,接着将 NodeNew 的后指针指向 NodeB
    在这里插入图片描述

  • 此时,线程C 进来了,线程 C 和线程 B 的执行流程很类似,都是执行 acquire() 中的方法
    在这里插入图片描述

  • 但是在 addWaiter() 方法中,执行流程有些区别。此时 tail != null,因此在 addWaiter() 方法中就已经将 nodeC 添加至队尾了,不需要再执行 enq(node) 方法

    在这里插入图片描述

  • NodeC 入队示意图
    在这里插入图片描述

3.7.5 acquireQueued(node, arg)

执行完 addWaiter() 方法之后,就该执行 acquireQueued() 方法了
在这里插入图片描述
在这里插入图片描述

  • 线程 B 的执行流程

    • 线程 B 执行 addWaiter() 方法之后,就进入了 acquireQueued() 方法中,此时传入的参数为封装了线程 B 的 NodeB

    • NodeB 的前驱结点为哨兵节点,因此 node.predecessor() 获取到的哨兵节点。

    • 哨兵节点满足 p == head,但是线程 B 执行 tryAcquire(arg) 方法尝试抢占 lock 锁时还是会失败,因此会执行下面 if 判断中的 shouldParkAfterFailedAcquire(p, node) 方法

    • 哨兵节点的 waitStatus == 0,因此执行 CAS 操作将哨兵节点的 waitStatus 改为 Node.SIGNAL(-1)
      在这里插入图片描述

    • 执行完毕将退出 if 判断,又会重新进入 for( ; ; ) 循环,此时执行 shouldParkAfterFailedAcquire(p, node) 方法时会返回 true,因此此时会接着执行 parkAndCheckInterrupt() 方法在这里插入图片描述

    • 线程 B 调用 park() 方法后被挂起,程序不会然续向下执行,程序就在这儿排队等待
      在这里插入图片描述

  • 线程 C 的执行流程,大致相同

3.7.6 unlock()

线程 A 执行 unlock() 方法

  • A 线程终于要 unlock() 了吗?真不容易啊!
    在这里插入图片描述

  • unlock() 方法调用了 sync.release(1) 方法
    在这里插入图片描述

  • release() 方法的执行流程
    在这里插入图片描述

  • 执行完上述操作后,当前占用 lock 锁的线程为 null,哨兵节点的 waitStatus 设置为 0,state 的值为 0(表示当前没有任何线程占用 lock 锁)
    在这里插入图片描述

杀个回马枪:继续来看 B 线程被唤醒之后的执行逻辑

  • 再次回到 lock() 方法的执行流程中来,线程 B 被 unpark() 之后将不再阻塞,继续执行下面的程序
    在这里插入图片描述

  • 执行完 setHead(node) 方法的状态如下图所示
    在这里插入图片描述

  • 将 p.next 设置为 null,这是原来的哨兵节点就是完全孤立的一个节点,此时 nodeB 作为新的哨兵节点
    在这里插入图片描述

3.7、AQS 总结

3.7.1 AQS 的考点

  1. 第一个考点:我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?

    答:3个状态:没占用是0,占用了是1,大于1是可重入锁

  2. 第二个考点:如果锁正在被占用,AB两个线程进来了以后,请问这个总共有多少个Node节点?

    答:答案是3个,分别是哨兵节点、nodeA、nodeB

3.7.2 AQS 源码解读案例图示

在这里插入图片描述


http://www.ppmy.cn/news/48280.html

相关文章

二叉树的堂兄弟节点

目录 一.二叉树的堂兄弟节点 1.题目描述 2.问题分析 3.代码实现 1.BFS解法 2.DFS解法 二.二叉树的堂兄弟节点 II 1.题目描述 2.问题分析 3.代码实现 一.二叉树的堂兄弟节点 1.题目描述 在二叉树中&#xff0c;根节点位于深度 0 处&#xff0c;每个深度为 k 的节点的子…

JUC并发编程之CompletableFuture

Future future是java5新加的一个接口&#xff0c;他提供了一种异步并行计算的功能 接口定义了操作异步任务执行的一些方法&#xff0c;如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务是否执行完毕 目的&#xff1a;异步多线程执行且有返回结果&#…

腾讯云轻量级云服务器Centos7防火墙开放8080端口

腾讯云轻量级云服务器Centos7防火墙开放8080端口 一、centos7防火墙打开端口 因为Centos7以上用firewalld代替了iptables,也就是说firewalld开通了8080端口应该就行了 1.查看8080是否已经放开 sudo firewall-cmd --permanent --zonepublic --list-ports2.查看防火墙状态 s…

ETCD(四)读请求处理过程

客户端通过etcdctl执行get命令 etcdctl get name --endpoints localhost:12379,192.158.00.32:12379client端 首先是client会解析这条命令&#xff0c;包括其中的get API方法&#xff0c;key值&#xff0c;请求server地址。解析完之后etcdctl会创建一个clientv3库对象&#xf…

基于C++开发的医院医学影像PACS 可二次开发,三维重建

医学影像PACS系统源码&#xff0c;集成三维影像后处理功能&#xff0c;包括三维多平面重建、三维容积重建、三维表面重建、三维虚拟内窥镜、最大/小密度投影、心脏动脉钙化分析等功能。系统功能强大&#xff0c;代码完整。有演示。 本套PACS系统专门针对医院工作流程设计的&am…

Redis---主从复制

一、redis主从复制 主从复制&#xff1a;是存储数据的服务结构 主服务器&#xff1a;接受客户端连接的服务器 从服务器&#xff1a;自动与主服务器保持数据一致的服务器 配置主从复制 1、环境准备 主服务器 主机名&#xff1a;master IP地址&#xff1a;192.168.11.101/…

企业想注销境外投资备案应该怎么做?

我们今天就来说一下关于境外投资备案的注销&#xff0c;有办理的需求&#xff0c;当然就有注销的需求。如果您当初想去海外投资并购一家公司&#xff0c;因此办理了境外投资备案&#xff0c;但是由于种种原因可能没有办法投资下去了&#xff0c;那么这个时候我们就需要做境外投…

git服务器搭建

ubuntu搭建git服务器 安装git&#xff1a;apt install git建立用户账户&#xff1a;用来存放git远程仓库&#xff0c;这里只是个ubuntu账户&#xff0c;没有特殊含义&#xff1b; adduser gitServer 设置账户密码&#xff1a; passwd gitServer 创建仓库目录&#xff1a;该目录…