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

文章目录

  • 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.mfbz.cn/a/14273.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【手把手做ROS2机器人系统开发一】开发环境搭建

【手把手做ROS2机器人系统开发一】开发环境搭建 目录 【手把手做ROS2机器人系统开发一】开发环境搭建 一、专栏介绍&#xff1a; 二、开发环境搭建&#xff1a; 1.Ubuntu系统安装 2.ROS2系统环境安装 3.测试系统运行 一、专栏介绍&#xff1a; 大家好&#xff0c;今天给大家…

栈的基本操作(C语言实现)创建,销毁,入栈,出栈

前言 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则。 压栈&#xff1a;栈的…

同样是测试,朋友到了30k,我才12K,这份测试面试8股文确实牛

程序猿在世人眼里已经成为高薪、为人忠诚的代名词。 然而&#xff0c;小编要说的是&#xff0c;不是所有的程序员工资都是一样的。 世人所不知的是同为程序猿&#xff0c;薪资的差别还是很大的。 众所周知&#xff0c;目前互联网行业是众多行业中薪资待遇最好的&#xff0c;…

Fedora 38 正式发布

Fedora Linux 38 正式发布&#xff0c;用户可以访问官网下载安装最新版本。 新网站 如果你点击了上面的官网链接&#xff0c;你应该会注意到 Fedora 的官网看起来与之前有了很大不同。这是 Fedora Websites & Apps 团队与 Design & Infrastructure 团队以及广大社区合作…

【视频课程】算法工程师需要的ChatGPT大模型算法理论与实践课程!非粗浅科普...

前言 自从2022年11月ChatGPT发布之后&#xff0c;迅速火遍全球。其对话的交互方式&#xff0c;能够回答问题&#xff0c;承认错误&#xff0c;拒绝不适当的请求&#xff0c;高质量的回答&#xff0c;极度贴近人的思维的交流方式&#xff0c;让大家直呼上瘾&#xff0c;更是带火…

安装配置SVN版本控制管理工具

SVN工具能帮我们做什么&#xff1f; 核心功能&#xff1a;文档版本管理系统 适合对象&#xff1a;个人与团队都可以使用&#xff0c;企业中项目资源的重要管理工具 举例&#xff1a;一个文件夹里面的文档管理 1.下载安装SVN服务器 VisualSVN-Server 2.下载安装SVN客户端 T…

<网络编程>网络套接字

目录 理解源IP地址和目的IP地址 认识端口号 端口号和进程ID的关系 理解源端口号和目的端口号 初步认识TCP、UDP协议 TCP协议 UDP协议 网络字节序列 socket网络接口 socket常见API sockaddr结构 UDPsocket 编码&#xff1a; 理解源IP地址和目的IP地址 源IP&#xf…

Jupyter Notebook的安装与使用

Jupyter Notebook Jupyter Notebook介绍Jupyter Notebook使用安装启动创建文件编写代码和文本常用命令配置文件 Anaconda Jupyter Notebook介绍 Jupyter Notebook是一个基于Web的交互式计算环境&#xff0c;可以让用户以文档形式记录代码、数据分析结果和说明文本&#xff0c;并…

从零开始的ChatGLM 配置详细教程

从零开始的ChatGLM配置教程 文章目录 从零开始的ChatGLM配置教程一&#xff0c;前言二&#xff0c;环境配置1、下载ChatGLM项目2、配置程序运行环境 三、在HuggingFace下载chatGLM-6B模型1&#xff0c;安装 Git Lfs2&#xff0c;下载相关文件3&#xff0c;在HuggingFace中下载相…

一致性 Hash 算法 及Java TreeMap 实现

1、一致性 Hash 算法原理 一致性 Hash 算法通过构建环状的 Hash 空间替线性 Hash 空间的方法解决了这个问题&#xff0c;整个 Hash 空间被构建成一个首位相接的环。 其具体的构造过程为&#xff1a; 先构造一个长度为 2^32 的一致性 Hash 环计算每个缓存服务器的 Hash 值&…

基于Java+Spring+vue+element实现旅游信息管理平台系统

基于JavaSpringvueelement实现旅游信息管理平台系统 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文…

抢先看,甘特图工具DHTMLX gantt 灯箱编辑器通过套件 UI 小部件进行了扩展

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…

为什么重写equals时必须重写hashCode()

不重写equals和不重写 hashCode()之前&#xff1a;equals()比较的是对象的内存地址&#xff0c;hashCode()比较的其实也是内存地址(内存地址输入到哈希函数中得到的整数) 重写了之后&#xff0c;equals()比较的是对象的内容值&#xff0c;如果hashCode()不重写&#xff0c;还是…

Android硬件通信之 WIFI通信

一&#xff0c;简介 1.1 随着网络的普及和通信技术的发展&#xff0c;网络的传输速度也越来越快&#xff0c;wifi技术也还成为手机设备最基本的配置。我们可以通过wifi实现手机与手机之前的信息传输&#xff0c;当然也可以与任意一台有wifi模块的其它设备传输。 1.2 wifi与蓝…

【数据库多表操作】sql语句基础及进阶

常用数据库&#xff1a; 数据库&#xff08;Database&#xff09;是按照数据结构来组织、存储和管理数据的仓库&#xff0c;它是长期存储在计算机内、有组织、有结构的数据集合。数据库是信息系统的核心部分&#xff0c;现代软件系统中大量采用了数据库管理系统&#xff08;DBM…

黑马在线教育数仓实战7

1. hive的相关的优化 1.1 hive的相关的函数(补充说明) if函数: 作用: 用于进行逻辑判断操作语法: if(条件, true返回信息,false返回信息) 注意: if函数支持嵌套使用 nvl函数: 作用: null值替换函数格式: nvl(T value, T default_value) COALESCE函数 作用: 非空查找函数:格式…

Node【Express框架【二】】

文章目录 &#x1f31f;前言&#x1f31f;中间件&#x1f31f;中间件函数&#x1f31f;什么是中间件函数&#x1f31f;中间件函数可以做什么 &#x1f31f;Express中间件的类型&#x1f31f;应用级中间件&#x1f31f;路由器级中间件&#x1f31f;错误处理中间件&#x1f31f;内…

华为OD机试真题(Java),计算最大乘积(100%通过+复盘思路)

一、题目描述 给定一个元素类型为小写字符串的数组&#xff0c;请计算两个没有相同字符的元素长度乘积的最大值&#xff0c; 如果没有符合条件的两个元素&#xff0c;返回0。 二、输入描述 输入为一个半角逗号分隔的小写字符串的数组&#xff0c;2 < 数组长度<100&am…

设计模式 --- 概述

一、设计模式概述 1.1、软件设计模式的产生背景 "设计模式"最初并不是出现在软件设计中&#xff0c;而是被用于建筑领域的设计中。 1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任 克里斯托夫亚历山大 &#xff08;Christopher Alexander&…

机器学习算法 决策树

文章目录 一、决策树的原理二、决策树的构建2.1 ID3算法构建决策树2.2 C4.5 算法树的构建2.3 CART 树的创建 三、决策树的优缺点 一、决策树的原理 决策树&#xff08;Decision Tree&#xff09;是一种非参数的有监督学习方法&#xff0c;它能够从一系列有特征和标签的数据中总…