• 深入理解AQS


    深入理解AQS

    前言

    AQS 是什么?

    AQS 指的是 Java中对管程模型的一种抽象实现,和synchronized一样都是对管程模型的实现。
    只不过在我们 Java 中为了补充 synchronized锁的缺陷,提供了Lock锁,而 AQS 是对这个锁的一个抽象,将线程的一个竞争,加锁解锁,都和不同的锁子类抽象出来,在抽象类中将加锁和解锁以及阻塞,唤醒,线程排队队列做了一个抽象,即AbstractQueuedSynchronizer类。

    MESA模型

    在管程的发展史上,有三种管程模型,分别是Hasen模型、Hoare模型和 MESA模型。现在正在广泛使用的是MESA模型。

    目前使用最广泛的是 MESA 管程模型。

    在这里插入图片描述

    管程中引入了条件变量,每个条件变量都有一个等待队列,作用是解决线程之间同步问题。

    • wait() 等待

    如:我们synchronized锁中 wait()
    wait()方法还有一个超时参数,为了避免线程进入等待 队列永久阻塞

    我们平时通过一定的条件变量,让线程去等待

    //条件
       while (wirteLockNum > 0) {
                    lock.wait();
                }
    
    • 1
    • 2
    • 3
    • 4

    唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不 满足了,所以循环检验条件。

    • 与之对应 notify()和notifyAll() 唤醒
    lock.notify();
    lock.notifyAll();
    
    • 1
    • 2

    synchronized

    Java语言的内置管程synchronized 是基于 JVM 实现的,使用 c++语言实现,Java中又叫 Monitor 锁,监视器

    在这里插入图片描述

    java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖 于 ObjectMonitor 实现,这是 JVM 内部基于 C++ 实现的一套机制。

    这也就是为什么 Java 中将线程等待,唤醒等方法放在顶级父类中的原因,跟 AQS 和不同的是,synchronized是将等待队列以及同步队列都封装到了 JVM 中。

    同理:

    C++中一个对象也就是 Java中 Object 对象中,当然是基于 JVM 层面来说,将锁所需要的信息都实现在了 C++ ObjectMonitor对象中

    包括同步队列,重入次数,等待队列,以及对象锁的地址,也包括1.5之后优化的一些信息,如线程id,线程地址等。

    ObjectMonitor() { 
     _header = NULL; //对象头 markOop 
      _count = 0; 
       _waiters = 0,
        _recursions = 0; // 锁的重入次数
         _object = NULL; //存储锁对象 
          _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
     _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
      _WaitSetLock = 0 ; 
      _Responsible = NULL ; _succ = NULL ; 
      _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构) 
       FreeNext = NULL ; 
        _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程) 
         _SpinFreq = 0 ; 
          _SpinClock = 0 ; 
          OwnerIsThread = 0 ; 
          _previous_owner_tid = 0; 
        
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    执行顺序在这里插入图片描述

    在获取锁时,是将当前线程插入到cxq的头部
    而释放锁时,默认策略(QMode=0)是:
    如果EntryList为空,则将 cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取 锁。

    _EntryList不为空,直接从_EntryList中唤醒线程。

    这里对象头的信息以及1.5之后锁升级的一些细节就不细说了。

    深入理解AQS

    AQS 对管程模型实现

    锁实现

    定义了一些锁行为接口

    加锁,解锁,可打断锁,是否加锁状态,尝试加锁,获取条件队列(用条件变量来控制线程)

    在这里插入图片描述

    管程模型中谁拥有锁

    AbstractOwnableSynchronizer 抽象出一个接口,拥有锁。
    在这里插入图片描述

    抽象实现

    AbstractQueuedSynchronizer 抽象实现类中需要如下几个条件

    • 首先锁的变量
    • 同步等待队列
    • 资源模式(一般是独占锁,共享锁,如sys独占锁)
    • 条件等待队列

    MESA 大致模型需要条件基本满足

    但是 AQS抽象队列 为我们考虑的很多,包括是否公平锁,是否独占锁,资源状态为了后续是否可以打断锁等。

    在这里插入图片描述
    JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的 一般是通过一个内部类Sync继承 AQS 将同步器所有调用都映射到Sync对应的方法。

    AQS具备特性

    • 阻塞等待对垒
    • 共享or独占
    • 公平or非公平
    • 可重入
    • 可允许打断

    以及 AQS 中锁的可用状态

    • state
      多个线程下只能由一个更改获取成功,AQS 中使用 CAS 进行更改。

    访问以及修改方式

    在这里插入图片描述

    锁访问方式,共享or独占以及 队列中节点转态
    在这里插入图片描述

      1. 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
      1. CANCELLED,值为1,表示当前的线程被取消;
      1. SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
      1. CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列 中;
      1. PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

    不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享 资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出 队等),AQS已经在抽象层实现好了。

    自定义同步器实现时主要实现以下几种方法:

    • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现 它。
    • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
    • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
    • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但 没有剩余可用资源;正数表示成功,且有剩余资源。
    • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待 结点返回true,否则返回false。

    同步等待队列i

    AQS 中使用双向链表用于同步队列等待队列,同里也是多个线程竞争会使用 CAS 来操作,确保一个线程更改成功,防止其他线程更改。

    AQS 抽象类中定义的双向队列 Node节点

    在这里插入图片描述

    • 1 多个线程Lock 加锁 cas对变量state进行修改为1
    • 2 修改成功,获取锁,设置当前拥有锁线程,后期再重入可判断是否重入
    • 3 修改失败,进行入队,cas第一个阻塞线程构建双向链表,入队,设置头尾节点,pakr当前线程
    • 4 设置头结点节点变量为-1,用于唤醒同步队列后面线程。
    • 5 解锁,将当前拥有线程地址设置null,cas 将state变量修改为 0(可重入-1),然后unpark当前线程
    • 6 其他被unpark的线程被唤醒开始cas尝试更改state 成功即拿到锁,设置当前拥有线程,更改头结点指向自己节点,将前置节点不指向自己即设置为null,前置节点next 不指向自己节点指向 Null。
    • 7 依次类推

    AQS 具体加锁解锁流程

    AQS 具体实现锁有很多,这里我们主要说一下 AQS 的流程对 MESA 的流程过程。

    说的再多不如上代码演示一遍

    
        private static  int sum = 0;
        private static Lock lock = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
    
            for (int i = 0; i < 3; i++) {
                Thread thread = new Thread(()->{
                    //加锁
                    lock.lock();
                    try {
                        // 临界区代码
                        // 业务逻辑
                        for (int j = 0; j < 10000; j++) {
                            sum++;
                        }
                    } finally {
                        // 解锁
                        lock.unlock();
                    }
                });
                thread.start();
            }
    
            Thread.sleep(20000);
            System.out.println("sum : " + sum);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    这里我们来debug跟一遍流程
    
    • 1

    首先main启动 对所有thread debug

    这里有三个线程,都在lock上阻塞
    在这里插入图片描述

    t0 线程开始,此时没有其他线程对state 更改,t0更改成功 然后设置 exclusiveOwnerThread 为t0线程

    在这里插入图片描述

    我们可以看到期望 0 更改 1 肯定是会成功的
    在这里插入图片描述
    成功进来 设置 exclusiveOwnerThread
    在这里插入图片描述

    t0获取锁成功 进来
    在这里插入图片描述

    然后切t1线程
    在这里插入图片描述

    进入获取锁方法acquire
    在这里插入图片描述

    可以看到尝试获取锁,肯定获取失败,t0已经拿到锁了
    在这里插入图片描述

    此时构建双向链表,此时头尾结点是null

    在这里插入图片描述

    所以会走下面构建 Node 节点方法

    先构建一个线程是t1的node节点 Node node = new Node(Thread.currentThread(), mode);
    在这里插入图片描述

    尾结点是null 所以肯定是先构建一个空的头 node节点
    在这里插入图片描述

    再次循环将刚才构建的节点,next指向 t1线程 Node 节点,同事t1node的pre指向 头节点

    在这里插入图片描述

    cas设置头尾结点
    在这里插入图片描述

    继续执行 acquireQueued 方法 获取头节点,尝试获取锁

    在这里插入图片描述

    获取失败,当前线程入队完成,需要阻塞,设置前置节点 waitStatus 是-1,用来唤醒之后的节点线程

    在这里插入图片描述

    这里cas设置
    在这里插入图片描述
    成功返回
    在这里插入图片描述
    parkAndCheckInterrupt() 然后阻塞当前线程 ,将t1线程挂起

    在这里插入图片描述

    此时可看到 只剩t0 t2 线程 ,t0线程未释放锁,t1 已经被挂起
    在这里插入图片描述

    t2 还在准备获取锁
    在这里插入图片描述

    继续往下执行 t2

    最终也是构建t2 node 节点

    在这里插入图片描述
    这时候尾结点不是空节点而是 t1线程 Node 节点,cas 设置尾结点指向t2线程 Node 节点,t2 pre 指向t1节点也就是当前保存t1节点的尾结点

    在这里插入图片描述

    可以看到
    在这里插入图片描述

    继续调用 enq 入队 继续 acquireQueued 方法 设置前置节点t1 node节点 为 -1

    在这里插入图片描述

    然后 park 阻塞

    最后来看解锁 t0释放锁

    在这里插入图片描述

    可以 看到t1被唤醒了

    在这里插入图片描述

    重置中断标识
    在这里插入图片描述
    在这里插入图片描述

    继续执行
    在这里插入图片描述

    继续循环
    在这里插入图片描述

    获取前置节点以及尝试获取锁
    在这里插入图片描述

    设置 头节点以及将t1 node 节点pre 不指向 头节点 ,头结点next不指向t1 node 节点

    在这里插入图片描述

    t1拿到锁进入 临界区
    在这里插入图片描述

    依次执行t3也是如此

    具体流畅图如下:

    在这里插入图片描述

    解锁流程图

    在这里插入图片描述

    从线程竞争到入队以及park unPark 整个流程

    释放锁方法

    在这里插入图片描述
    多次释放多次减releases 可重入锁解锁,加锁也是在Lock加锁方法里面
    可以看到只能自己解自己加的锁,同事解锁后设置 exclusiveOwnerThread 为 Null 。
    在这里插入图片描述

    打断线程

    ReentrantLock 打断方法
    在这里插入图片描述
    非公平锁里打断设置
    在这里插入图片描述

    真正打断方法
    如果当前线程拿到锁就打断 同时设置一些属性
    在这里插入图片描述

    这个方法里最终清除链表里的 线程 Node节点

    在这里插入图片描述

    实际上 打断方法传的就是这个值
    在这里插入图片描述
    可以看到对应节点的值

    在这里插入图片描述

    条件等待队列

    AQS中条件队列是使用单向列表保存的,用nextWaiter来连接: 调用await方法阻塞线程; 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条 件队列)

    在这里插入图片描述
    条件等待队列

    AQS中条件队列是使用单向列表保存的,用nextWaiter来连接: 调用await方法阻塞线程; 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条 件队列)

    1. 调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向 Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。

    2. 调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤 醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所 以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用 Condition#await方法而阻塞的线程。

    通知
    Node p = enq(node); 入队 同步队列
    在这里插入图片描述

    放入条件队列里,同时 park 阻塞
    在这里插入图片描述
    在这里插入图片描述
    条件变量waitStatus是 Node.CONDITION

    最后

    AQS 里指的我们学习一个东西,并发情况下,构建链表,以及链表节点入队操作。

    在这里插入图片描述

    入队
    在这里插入图片描述

    其他知识点

    Java 多线程基础
    ReentrantLock用法详解
    深入理解信号量Semaphore
    深入理解并发三大特性
    并发编程之深入理解CAS
    深入理解CountDownLatch
    Java 线程池

  • 相关阅读:
    Talk预告 | 微信AI高级研究员苏辉:微信AI大规模预训练语言模型WeLM
    外汇天眼:一平台产品1天收益率20%,7天1倍!FCA已发出警告!
    腾讯发布 2022 年季度财报,员工月薪 85473 元,网友看完炸了...
    6轮面试阿里Android开发offer,薪资却从21k降到17k,在逗我?
    统计字符出现次数类Counter
    Android 的Memory Profiler详解
    数据结构-单链表操作
    普通Java类成员变量应该用private还是public
    后端返回parentId,前端处理成children嵌套数据
    13【触发器】
  • 原文地址:https://blog.csdn.net/weixin_38361347/article/details/127605895