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

news2024/12/24 9:48:27

文章目录

  • 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 线程唤醒线程 A
            System.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 源码解读案例图示

《尚硅谷Java大厂面试题第3季》- 周阳 - AQS 流程图:https://www.processon.com/view/link/64449ade84b4b71c14fe1079

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/450308.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

本地搭建属于自己的ChatGPT:基于PyTorch+ChatGLM-6b+Streamlit+QDrant+DuckDuckGo

本地部署chatglm及缓解时效性问题的思路&#xff1a; 模型使用chatglm-6b 4bit&#xff0c;推理使用hugging face&#xff0c;前端应用使用streamlit或者gradio。 微调对显存要求较高&#xff0c;还没试验。可以结合LoRA进行微调。 缓解时效性问题&#xff1a;通过本地数据库…

YOLOv7如何提高目标检测的速度和精度,基于模型结构提高目标检测速度

目录 一、目标检测二、目标检测的速度和精度的权衡1、速度和精度的概念和定义2、如何评估目标检测算法的速度和精度3、速度和精度之间的权衡 三、基于模型结构提高目标检测速度1、Backbone网络的选择2、特征金字塔网络的设计3、通道注意力机制4、混合精度训练 一、目标检测 目…

光纤网卡传输速率和它的应用领域有哪些呢?通常会用到哪些型号网络变压器呢?

Hqst盈盛&#xff08;华强盛&#xff09;电子导读&#xff1a;常有客户问起光纤网卡该如何选用到合适的产品&#xff0c;选用时要注意到哪些事项&#xff0c;这节将结合配合到的网络变压器和大家一起探讨&#xff0c;希望对大家有些帮助。 1&#xff0e;光纤网卡传输速率与网络…

【教程】一文读懂 ChatGPT API 接入指南

ChatGPT 是一个基于自然语言处理技术的 API&#xff0c;它能够根据用户的输入&#xff0c;生成智能回复。结合当前最先进的AI技术&#xff0c;AP智能续写&承接上下文&#xff1b;可以回答各种问题&#xff0c;例如&#xff1a;历史&#xff0c;科学&#xff0c;文化&#x…

【越早知道越好】的道理——能够提高效率的【快捷键】

文章目录 1️⃣虚拟桌面⚜️第一步&#xff1a;打开任务视图⚜️第二步&#xff1a;创建桌面⚜️第三步&#xff1a;桌面切换⚜️第四步&#xff1a;桌面删除 2️⃣窗口切换3️⃣桌面分屏⚜️如何分屏 前言&#x1f9d1;‍&#x1f3a4;&#xff1a;作为程序员&#x1f468;‍&…

15天学习MySQL计划-多表联查(基础篇)第四天

15天学习MySQL计划&#xff08;多表联查&#xff09;第四天 1.多表查询 1.1概述 ​ 指从多张表中查询数据 ​ 在项目开发中&#xff0c;在进行数据库表结构设计时&#xff0c;会根据业务需求及业务模块之间的关系&#xff0c;分析并设计表结构&#xff0c;由于业务之间相互…

大数据实战 --- 美团外卖平台数据分析

目录 开发环境 数据描述 功能需求 数据准备 数据分析 RDD操作 Spark SQL操作 创建Hbase数据表 创建外部表 统计查询 开发环境 HadoopHiveSparkHBase 启动Hadoop&#xff1a;start-all.sh 启动zookeeper&#xff1a;zkServer.sh start 启动Hive&#xff1a; nohup …

人工智能会影响测试工程师吗

并不是危言耸听 当下最火的是什么&#xff0c;那非ChatGPT莫属了&#xff0c;以ChatGPT为代表的各类AIGC工具&#xff0c;在不断颠覆我们的认知&#xff0c;不仅能完成律师&#xff0c;医学考试&#xff1b;还能画出一张精美的设计图&#xff0c;拿下艺术大赛一等奖。 以之对…

C#基础学习--反射和特性

元数据和反射 要使用反射&#xff0c;必须使用System.Reflection 命名空间 Type类 Type是一个抽象类&#xff0c;用来包含类型的特性&#xff0c;使用这个类的对象可以让我们获取程序使用的类型的信息 我们可以从Type对象中获取需要了解的有关类型的几乎所有信息 获取Type对象…

Node.js下载安装及环境配置教程

一、进入官网地址下载安装包 https://nodejs.org/zh-cn/download/ 选择对应你系统的Node.js版本&#xff0c;这里我选择的是Windows系统、64位 Tips&#xff1a;如果想下载指定版本&#xff0c;点击【以往的版本】&#xff0c;即可选择自己想要的版本下载 二、安装程序 &…

在 VSCode 中让 TypeScript 错误更漂亮且易于阅读

简介 TypeScript 是一种流行的编程语言&#xff0c;为 JavaScript 提供了静态类型和改进的错误检测。然而&#xff0c;随着类型的复杂性增加&#xff0c;错误的复杂性也增加了。这就是 Pretty TypeScript Errors VSCode 插件的用途&#xff0c;它可以在 Visual Studio Code 中…

8.线性搜索算法和二进制搜索算法

算法&#xff1a;线性搜索算法 线性搜索是一种非常简单的搜索算法。在这种类型的搜索中&#xff0c;逐个对所有项目进行顺序搜索。检查每个项目&#xff0c;如果找到匹配项&#xff0c;则返回该特定项目&#xff0c;否则搜索将继续&#xff0c;直到数据收集结束。 算法 Linea…

【数据结构】- 链表之单链表(下)

文章目录 前言一、单链表(下)1.1 查找修改1.2 在任意位置插入1.2.1 在pos位置插入(也就是pos位置之前)1.2.2 在pos位置之后插入 1.3 在任意位置删除1.3.1 删除pos位置得值1.3.2 删除pos位置后面的值 二、完整代码总结 前言 未来藏在迷雾中 叫人看来胆怯 带你踏足其中 就会云开…

【C++类和对象】类和对象(中):拷贝构造函数 {拷贝构造函数的概念及特征,拷贝构造函数不能使用传值传参,编译器自动生成的拷贝构造函数}

四、拷贝构造函数 4.1 概念 在创建对象时&#xff0c;可否创建一个与已存在对象一某一样的新对象呢&#xff1f; 拷贝构造函数&#xff1a;只有单个形参&#xff0c;该形参是对本类类型对象的引用(一般常用const修饰)&#xff0c;在用已存在的类类型对象创建新对象时由编译器…

MySQL高级(二)

一、SQL优化 &#xff08;一&#xff09;插入数据 批量插入 多次插入每一次insert都要与数据库建立连接。 INSERT INTO 表名 VALUES (),(),(); 一次插入数据不宜过多&#xff0c;不要超过1000条。 手动提交事务 START TRANSACTION; INSERT INTO 表名 VALUES (),(),(); I…

车载以太网 - SomeIP - 协议用例 - Format_01

目录 1、验证Client ID字段静态设置为0x0000 2、验证Session ID字段静态设置为0x0001 3、验证Protocol Version字段静态设置为0x01

SpringCloud:ElasticSearch之自动补全

当用户在搜索框输入字符时&#xff0c;我们应该提示出与该字符有关的搜索项&#xff0c;如图&#xff1a; 这种根据用户输入的字母&#xff0c;提示完整词条的功能&#xff0c;就是自动补全了。 因为需要根据拼音字母来推断&#xff0c;因此要用到拼音分词功能。 1.拼音分词器…

【移动端网页布局】移动端网页布局基础概念 ④ ( 物理像素 | 物理像素比 | 代码示例 - 100 像素在 PC浏览器 / 移动端浏览器 显示效果 )

文章目录 一、物理像素 / 物理像素比二、代码示例 - 100 像素在 PC浏览器 / 移动端浏览器 显示效果 一、物理像素 / 物理像素比 移动端 网页开发 与 PC 端开发有很多不同之处 , 在图片处理方向需要采用 二倍图 / 三倍图 / 多倍图 方式进行图片处理 ; 图片处理的方式与如下的 物…

项目支付接入支付宝【沙箱环境】

前言 订单支付接入支付宝&#xff0c;使用支付宝提供的沙箱机制模拟为订单付款。我这里主要记录一下沙箱环境如何接入到系统中&#xff0c;具体细节的实现。按照官方文档来就可以了。 1、使用步骤 这里有几个重要数据要拿到&#xff0c;一个是支付宝的公钥和私钥&#xff0c…

ClickHouse监控系统Prometheus+Grafana

目录 1 PrometheusGrafana概述2 安装Prometheus Grafana3 配置ClickHouse4 配置Grafana 1 PrometheusGrafana概述 ClickHouse 运行时会将一些个自身的运行状态记录到众多系统表中( system.*)。所以我们对于 CH 自身的一些运行指标的监控数据&#xff0c;也主要来自这些系统表。…