JUC面试(十二)——AQS

news2024/11/27 23:46:45

AQS

juc.locks包下

AbstractQueuedSynchronizer,抽象的队列同步器

在这里插入图片描述

aqs是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态,0未占用,大于0被占用。

解释:

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

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

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

在这里插入图片描述

AQS相关

在这里插入图片描述

Semaphore、CyclicBarrier、ReentrantReadWriteLock、CountDownLatch、ReentrantLock等juc类都是AQS实现的,都有sync内部类。

在这里插入图片描述

可以说,AQS是锁的实现者,是Java并发大神Douglee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

AQS初步知识

在这里插入图片描述

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

在这里插入图片描述

在这里插入图片描述

state变量+CLH双端Node队列

int变量:

AQS的同步状态State成员变量

相当于银行办理业务的受理窗口状态,零就是没人,自由状态可以办理,大于等于1,有人占用窗口,等着去。

CLH队列队列:

CLH队列(三个大牛的名字组成),为一个双向队列,排队线程

在这里插入图片描述

内部类Node

Node类在AQS类内部,结构如下:

在这里插入图片描述

在这里插入图片描述

队列中每个排队的个体就是一个Node,Node的等待状态waitState成员变量,一个Node相当于一个Thread线程(有volatile Thread变量)。

属性说明:

在这里插入图片描述

在这里插入图片描述

同步队列的基本结构

在这里插入图片描述

AQS底层是用LockSupport.park()来进行排队的

ReentrantLock开始解读AQS

Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】Sync的子类完成线程访问控制的

在这里插入图片描述

公平锁和非公平锁

ReentrantLock底层实现:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法,是否排队。

在这里插入图片描述

解读非公平锁方法lock()中的AQS

底层AQS源码深度分析

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

lock()方法:

state变量为0,则未锁住,其中一个线程A抢占cpu,置为1,CAS比较并交换(usafe类)

在这里插入图片描述

acquire()方法(AQS类中):

cpu被抢占了,state > 0,后面线程B,C等待,准备入队列

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

一共有3个方法:

tryAcquire()

尝试获取锁,抢占。模板设计模式,子类不重写,就抛出异常,强制重写。

在这里插入图片描述

非公平锁的重写方法底层是nonfairTryAcquire方法:

    /**
      * Performs non-fair tryLock.  tryAcquire is implemented in
      * subclasses, but both need nonfair try for trylock method.
      */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();

        int c = getState();  
        //刚好cpu空闲,线程B正好抢到
        if (c == 0) { 
            if (compareAndSetState(0, acquires)) {    
                setExclusiveOwnerThread(current);  
                return true;
            }         
        }

        //可重入锁,抢到cpu的线程跟当前线程是同一个则获取到锁,state+1
        else if (current == getExclusiveOwnerThread()) {      
            int nextc = c + acquires;
            if (nextc < 0) // overflow       
                throw new Error("Maximum lock count exceeded");  

            setState(nextc);    
            return true;    
        }      
        return false;  
    }
addWaiter(Node.EXCLUSIVE)

EXCLUSIVE排它,队列是双向队列

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //尾结点不为空,队列不为空
        Node pred = tail;
        if (pred != null) {
            //线程C指向尾结点B
            node.prev = pred;
            //设置线程C为尾结点
            if (compareAndSetTail(pred, node)) {
                //尾结点B,之前的尾结点B下一个结点为C结点
                pred.next = node;
                return node;
            }
        }
        
        //第一个到达队列的线程,执行这里,进队列
        enq(node);
        return node;
    }

双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的。enq(node)添加Node结点到队列:

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //第一次进队列的头结点不是当前线程结点,是一个空节点,叫做哨兵结点
                if (compareAndSetHead(new Node()))
                    //尾结点也指向哨兵结点
                    tail = head;
            } else {
                //有了哨兵结点,执行,当前结点的前指针指向尾结点
                node.prev = t;
                //设置新进的线程结点为尾结点
                if (compareAndSetTail(t, node)) {
                    //上一个尾结点的next指针指向最新的尾结点,即是当前线程结点
                    t.next = node;
                    return t;
                }
            }
        }
    }

第一个线程B到达队列后执行如下:

在这里插入图片描述

第二个线程C入队列:

在这里插入图片描述

acquireQueued()
    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //线程中断标志
            boolean interrupted = false;
            //自旋
            for (;;) {
                //获取当前线程的上一个结点
                final Node p = node.predecessor();
                //p == head,说明是第一个线程结点(头结点是空节点),出列再次尝试抢占cpu
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //尝试抢占cpu失败,LockSupport,park阻塞当前线程,等待被唤醒,唤醒后再次自旋tryAcquire去尝试获取cpu
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

park阻塞当前线程前再次尝试抢占cpu,只有前一结点状态是-1,才执行阻塞,方法shouldParkAfterFailedAcquire:

    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //当前结点的prev结点状态,起始0
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            //上一结点的结点状态设置为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

只有上一结点的状态为SIGNAL -1时,才会执行parkAndCheckInterrupt()方法park阻塞当前线程:

   /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        //卡死在这,等待被唤醒
        LockSupport.park(this);
        return Thread.interrupted();
    }

当占用cpu的线程A被释放时,线程B出队列,设置线程B结点的thread为null,变为哨兵结点,具体方法setHead():

    /**
     * Sets head of queue to be node, thus dequeuing. Called only by
     * acquire methods.  Also nulls out unused fields for sake of GC
     * and to suppress unnecessary signals and traversals.
     *
     * @param node the node
     */
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    } 

原哨兵结点等待被GC,线程B结点变新的哨兵结点:

在这里插入图片描述

解读非公平锁方法unLock()中的AQS

    public void unlock() {
        sync.release(1);
    }
    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            //释放成功
            //哨兵结点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //确定是阻塞状态
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease()方法是抛异常,模板设计模式方法,强制重写,ReentrantLock中内部类Sync重写。

    protected final boolean tryRelease(int releases) {
        //为0,则准备释放
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            //设置cpu占用线程为null,空闲,e s ku save bu
            setExclusiveOwnerThread(null);
        }
        //设置state=0,可重入锁一直减1
        setState(c);
        return free;
    }

唤醒线程结点方法unparkSuccessor():

    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        //头结点第一次进来设置状态为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        //实际的第一个线程结点
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //唤醒,回到被阻塞的程序中
        if (s != null)
            LockSupport.unpark(s.thread);
    }

AQS面试题

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

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

如果AB两个线程进来了以后,请问这个总共有多少个Node节点?

答案是3个,null的哨兵头结点

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

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

相关文章

极限运算法则——“高等数学”

各位CSDN的uu们你们好啊&#xff0c;今天&#xff0c;小雅兰学习的内容是极限运算法则 回顾 无穷小的极限运算法则 定理1&#xff1a;两个无穷小的和是无穷小 定理2&#xff1a;有界函数与无穷小的乘积是无穷小 极限的四则运算法则 定理3 定理4 定理5&#xff1a;极限的保序性…

实现自己的数据库二

一 前言上次数据库支持了一个测试表的插入和查询&#xff0c;但是数据全部保存到磁盘中的&#xff0c;如果程序重启后&#xff0c;数据都会全部丢了&#xff0c;所以需要持久化到磁盘上&#xff0c;像sqlite一样&#xff0c;简单的将数据库的数据保存到一个磁盘文件上。二 实现…

【BBuf的CUDA笔记】六,总结 FasterTransformer Encoder(BERT) 的cuda相关优化技巧

这里总结 FasterTransformer Encoder(BERT) 的cuda相关优化技巧 解读&#xff1a;https://github.com/NVIDIA/FasterTransformer/blob/main/docs/bert_guide.md &#xff0c;优化点解读之前是翻译了下 Faster Transformer BERT 的文档&#xff0c;然后省略了运行样例等环节&…

【Datewhale一起吃瓜 Task4】啃瓜第五章

支持向量机 任务&#xff1a;找到超平面 在样本空间中&#xff0c;找到最好的超平面把样本分开&#xff0c;即找到正中间的超平面 满足 该超平面 分开了两类该超平面 最大化支持向量间隔该超平面处于 间隔中间&#xff0c;到所有支持向量距离相等 如何找&#xff1a;表示出…

从聚水潭到金蝶云星空通过接口集成数据

从聚水潭到金蝶云星空通过接口集成数据数据源系统:聚水潭聚水潭成立于2014年&#xff0c;创始人兼CEO骆海东拥有近三十年传统及电商ERP的研发和实施部署经验。聚水潭创建之初&#xff0c;以电商SaaSERP切入市场&#xff0c;凭借出色的产品和服务&#xff0c;快速获得市场的肯定…

【论文简述】Attention-Aware Multi-View Stereo(CVPR 2020)

一、论文简述 1. 第一作者&#xff1a;Keyang Luo 2. 发表年份&#xff1a;2020 3. 发表期刊&#xff1a;CVPR 4. 关键词&#xff1a;MVS、代价体、注意力机制、正则化 5. 探索动机&#xff1a; However, the feature matching results from different channels are usual…

仿写Dubbo-MyRpc

基础 在仿写Dubbo之前&#xff0c;需要了解一些技术&#xff0c;像Java反射&#xff0c;Java代理&#xff0c;Java Socket以及Dubbo相关概念。 项目结构 项目gitee地址&#xff1a;https://gitee.com/AGi_R/framework my-common 整个项目的公共资源库。存放一些公共的注解&…

拦截器、过滤器、监听器

目录一、拦截器1. 拦截器是什么?2. 设置拦截器a. 定义拦截器b. 配置加载拦截器c. 新建页面二、过滤器1. 使用原因2. Filter概念图3. Filter编程三、监听器一、拦截器 拦截器&#xff1a;必须保证页面有访问controller的操作&#xff0c;否则拦截不了 1. 拦截器是什么? 概念…

OpenWrt软路由空间扩容

文章目录预备知识OpenWrt系统固件分类EXT4固件扩容方式新建分区扩容操作步骤直接扩容操作步骤SQUASHFS固件扩容方式新建分区扩容直接扩容EFI引导固件的额外操作参考预备知识 OpenWrt系统固件分类 EXT4固件 固件包名称中包含有ext4关键字&#xff0c;可以参考固件分类关键字示意…

设计模式 - 创建型模式_建造者模式

文章目录创建型模式概述Case模拟工程Bad ImplBetter Impl &#xff08;建造者模式重构代码&#xff09;小结创建型模式 创建型模式提供创建对象的机制&#xff0c; 能够提升已有代码的灵活性和可复⽤性。 类型实现要点工厂方法定义⼀个创建对象的接⼝&#xff0c;让其⼦类⾃⼰…

编写用户注册用表单

<!-- 需求&#xff1a; 用户注册&#xff1a;用户名、密码、确认密码、性别、兴趣爱好、学历、简介 --> <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>编写用户注册用表单<…

【Linux】Linux和Window下\r与\n的区别、git命令行的使用

作者&#xff1a;小卢 专栏&#xff1a;《Linux》、《Git》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 1. 回车换行符在Window下和在Linux下的区别&#xff1a; 1.1回车换行符&#xff1a;…

用友U8和旺店通企业版淘宝奇门单据接口集成

用友U8和旺店通企业奇门单据接口集成对接系统&#xff1a;旺店通企业奇门慧策最先以旺店通ERP切入商家核心管理痛点——订单管理&#xff0c;之后围绕电商经营管理中的核心管理诉求&#xff0c;先后布局流量获取、会员管理、仓库管理等其他重要经营模块。慧策的产品线从旺店通E…

实现宏offsetof()

本期介绍&#x1f356; 主要介绍&#xff1a;什么是offsetof()&#xff0c;offsetof()的用法&#xff0c;如何自己实现这个宏&#x1f440;。 offsetof其实是一个宏&#xff0c;作用是&#xff1a;能够求出指定成员相对于结构体起始地址的偏移量&#xff08;单位&#xff1a;字…

(考研湖科大教书匠计算机网络)第三章数据链路层-第一节:数据链路层概述

文章目录一&#xff1a;数据链路层概述&#xff08;1&#xff09;为什么要有数据链路层&#xff08;2&#xff09;数据链路层定义&#xff08;3&#xff09;点对点信道和广播信道二&#xff1a;数据链路层需要解决的一些问题&#xff08;1&#xff09;三个最基本问题①&#xf…

深入理解Promise之一步步教你手写Promise构造函数

目录前言一&#xff0c;手写教学1.1 基本结构1.2 resolve与reject结构搭建1.3 resolve与reject代码实现1.4 throw抛出异常改变状态1.5 promise对象状态只能转换一次1.6 then方法进行回调1.7 异步任务的回调执行1.8 执行多个回调的实现1.9 同步修改状态then方法结果返回1.10 异步…

【手写 Promise 源码】第四篇 - 翻译并理解 Promise A+ 规范

一&#xff0c;前言 上一篇&#xff0c;根据对 Promise 的分析和了解&#xff0c;实现了一个简版 Promise&#xff0c;主要涉及以下内容&#xff1a; Promise 的实现思路&#xff1b;Promise A 规范&#xff08;简版&#xff09;&#xff1b;Promise 简版实现和功能测试&…

KVM虚拟化之小型虚拟机kvmtool的使用记录

根据 kvmtool github仓库文档的描述&#xff0c;类似于QEMU&#xff0c;kvmtool是一个承载KVM Guest OS的 host os用户态虚拟机&#xff0c;作为一个纯的完全虚拟化的工具&#xff0c;它不需要修改guest os即可运行, 不过&#xff0c;由于KVM基于CPU的硬件虚拟化支持&#xff0…

读《哲学的故事》

文章目录读《哲学的故事》&#x1f6a9; 遇见&#x1f33b; 简述&#x1f33e; 部分摘抄读《哲学的故事》 一本书读过后&#xff0c;我有种脑子里又被塞进了很多新东西的感觉&#xff0c;也有种想要自我抒发、宣泄的欲望。可真到要说的时候&#xff0c;又好像无话可说。总归勉…

Java转换流(InputStreamReader/OutputStreamWriter)

文章目录概述为什么会有转换流&#xff1f;InputStreamReaderOutputStreamWriter概述 转换流是字节流到字符流的桥梁&#xff0c;在转换的过程中&#xff0c;可以指定编码。转换流也是一种处理流&#xff0c;它提供了字节流和字符流之间的转换。 转换流的两个类 InputStreamR…