首页 > 其他分享 >JUC之基-AQS详解

JUC之基-AQS详解

时间:2024-12-09 17:42:56浏览次数:5  
标签:Node JUC 结点 AQS 队列 阻塞 线程 之基

AQS

AQS是JUC学习的基石,是JUC中许多锁的底层实现机制,我们今天从ReentrantLock出发来深入源码解读AQS的设计。

AQS底层

AQS的几个重要属性:

    //阻塞队列的头
    private transient volatile Node head;
    //阻塞队列的尾
    private transient volatile Node tail;
    //核心属性,代表锁的状态(可以被各种子类实现成需要的机制)
    private volatile int state;
    //其父类属性,代表当前持有锁的线程
    private transient Thread exclusiveOwnerThread;

AQS有一个Node内部类,是将线程封装为了阻塞队列中的节点对象,几个重要属性:

    //下面几个都是线程的等待状态常量
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    //线程的等待状态
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    //封装的线程
    volatile Thread thread;

整体效果类似这样
image.png
当然,这里面的state,waitStatus我们还没有介绍含义,我们下面来介绍

state

state是AQS中最核心的一个属性,代表锁的状态,一般是为0代表可加锁,不为0代表已经有线程获得了锁,在ReentrantLock底层,实现的就是,如果有线程来就尝试cas将state由0变为1,cas成功代表加锁成功,如果锁重入那么state再加一即可。

waitStatus

waitStatus有5个值,分别是CANCELLED、SIGNAL、CONDITION、PROPAGATE和INITIAL,其中INITIAL实际上就是0,这里我加了状态INITIAL加以区分,真正源码中是没有这个状态的。我们今天要说的ReentrantLock只用到了SIGNAL和INITIAL状态,实际上,在阻塞队列中waitStatus为SIGNAL的Node有义务唤醒其后面的结点(这句话也许有点抽象,我们这里先给出一个最终阻塞队列中结点该有的状态)
image.png
事实上,当Thread-0抢到锁后,state变为1,Thread-1和Thread-2进入阻塞队列,阻塞队列会加一个Dummy结点(图中第一个线程为null的结点),这正对应我们刚才说的,“waitStatus为SIGNAL的Node有义务唤醒其后面的结点”,也就是必须有一个Dummy结点来唤醒阻塞队列中的Thread-1结点,而最后一个加入阻塞队列的Thread-2的waitStatus是INITIAL,因为他没有后一个结点,他的状态会在后一个结点插入队列中后被赋为SIGNAL

AQS行为

AQS作为一个锁框架,应该提供什么行为?要实现一个锁,需要实现哪些方法?我们应该考虑:

  1. 获得锁的策略?如何获得锁?
  2. 获得锁的线程将来如何释放锁?
  3. 获得不到锁的线程如何阻塞?
  4. 被唤醒的线程如何重新参与竞争锁?
    在AQS中,这几个问题都有了实现方案:

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

    线程首先通过tryAcquire()尝试获得锁,如果获得锁失败,通过acquireQueued(addWaiter(Node.EXCLUSIVE), arg)加入阻塞队列并park,而需要用户重写的是tryAcquire方法,即由用户来决定获取锁的方式,包括是否公平,是否支持重入锁等等,而获取不到锁的,AQS一律将其加入阻塞队列,并且AQS已经实现了阻塞的策略以及唤醒策略和唤醒后的重新抢占策略,并且对中断有了很好的处理。

     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运行过程

下面我们通过ReentrantLock来对AQS进行进一步介绍

  1. 默认非公平实现,new NonfairSync对象
  2. 调用lock方法,通过cas加锁(将state值从0变为1)
    如果加锁成功,代表获得锁,设置AQS中Owner为当前线程(Owner有什么用?)
    如果加锁失败,进入acquire(1)

    final void lock() {
             if (compareAndSetState(0, 1))
                 setExclusiveOwnerThread(Thread.currentThread());
             else
                 acquire(1);
         }
     public final void acquire(int arg) {
         if (!tryAcquire(arg) &&
             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
             selfInterrupt();
     }
  3. 调用tryAcquire(1),此处就是ReentrantLock自己的实现了,进入该方法,首先判断state是否为0,如果为0再次尝试cas,如果是重入锁,在这个方法里会返回true并将state加1
  4. 如果tryAcquire(1)返回false,代表获得锁失败,执行addWaiter(Node.EXCLUSIVE),此处参数代表独占锁(ReentrantLock是独占锁),这个方法的意义是将该线程加入到阻塞队列队尾等待,其中使用了经典的CAS自选volatile变量的方法
  5. acquireQueued真正负责了线程的阻塞,当addWaiter执行完毕后,线程已经进入队列,此时acquireQueued来对线程进行一些操作后阻塞,什么操作?还记得我们在前面说的线程等待状态吗,该方法会调用shouldParkAfterFailedAcquire方法,看当前线程在队列里的前一个线程是什么状态,如果是SIGNAL,就将当前线程park住,如果不是SIGNAL就通过cas将前一个线程的状态更新为SIGNAL,然后阻塞当前线程,也就是说,这个方法的实际作用是将该线程在队列中的前一个线程(在该线程加入队列前,这个线程是队尾,状态为INITIAL)状态改为SIGNAL并阻塞自己。这个方法还有另一个作用,我们来想,线程在这阻塞,那么将来不论是被打断还是被唤醒,首先还是在这个方法里运行,所以这个方法需要决定线程醒来之后做什么事,并且正确处理park线程被中断的情况。第一件事,处理线程醒来后做什么事,该方法规定,醒来的如果是阻塞队列中的第二个,那他就可以重新去获得锁(为什么是第二个?在下边我们通过图来介绍),如果不是第二个,就重新park。第二件事,如何处理因为中断醒来的park线程?同样的,如果是第二个,可以让其竞争锁,如果不是第二个,那就重新park。
    image.png
    我们来看为什么是第二个有资格去竞争锁,我们知道一开始阻塞队列中是有一个Dummy结点的,而这个结点的作用就是为了唤醒其后面的实际上的“第一个”结点,当Thread-1被唤醒并获得锁成功时,其前面的Dummy结点出队列,而Thread-1这个结点还在阻塞队列中,因为他有义务去唤醒他后面的结点,也就是说,阻塞队列中的第一个结点如果不是Dummy,代表第一个结点此时正持有锁,所以自然阻塞队列的第二个醒来后可以去竞争锁了。
  6. 接下来是释放锁的过程

    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中实际上是判断是否是重入锁,如果是就让state减一,如果不是就释放锁,释放锁成功后由该线程唤醒其在阻塞队列中的后一个结点,并进行unpark,之后被唤醒的结点又回到前面的操作,周而复始。

综上,我个人把AQS总结为,一个实现了完整的阻塞与唤醒策略的成熟的锁框架。

标签:Node,JUC,结点,AQS,队列,阻塞,线程,之基
From: https://www.cnblogs.com/ratelcloud/p/18595652

相关文章

  • JUC 多线程并发编程
    一、基本概念1.进程与线程进程(Process):计算机中正在运行的程序的实例,是操作系统分配资源的基本单位。每个进程拥有自己的内存空间、文件描述符、数据栈等。线程(Thread):进程中的一个执行单元。一个进程中至少有一个线程,通常称为主线程。线程是CPU调度和执行的最小单位。线程共......
  • 深入AQS底层源码
    1.AQS概述AQS是juc包下的一个抽象类,很多juc包下的工具类都是根据AQS是实现的,比如ThreadPoolExecutor、CountDownLatch、ReetrantLock、ReetrantWriteLock、Semaphore2.AQS的核心内容(1)核心属性state/***Thesynchronizationstate.*/privatevolat......
  • 线程和进程(juc)
    线程一:概念辨析1:线程与进程进程:1:程序由指令和数据组成,指令要执行,数据要读写,就需要将指令加载给cpu,把数据加载到内存,同时程序运行时还会使用磁盘,网络等资源。进程就是负责管理内存,加载指令,管理io的;2:当一个程序运行时就会将程序的相关代码加载到内存中,这就开启了一个进程......
  • 夜莺运维指南之基本部署
    对于一套监控系统而言,核心就是采集数据并存储,然后做告警判定、数据展示分析,整个流程图如下:1.夜莺服务端前置说明:各种环境的选型建议Dockercompose方式:可用于快速测试,不建议上生产,如果要生产环境使用Dockercompose,需要对Dockercompose真的很熟二进制部署:这是......
  • Java毕设之基于Uniapp+ssm基于微信小程序的社区团购购购物商城
    《[含文档+PPT+源码等]精品微信小程序基于Uniapp+ssm基于微信小程序的社区团购》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功以及课程答疑与微信售后交流群、送查重系统不限次数免费查重等福利!软件开发环境及开发工具:开发语言:Java后......
  • 泷羽sec学习--Burp Suite之基本介绍
     学习内容来自B站UP:泷羽sec微信公众号:泷羽sec一、基本介绍BurpSuite是一款用于Web应用程序安全测试的集成平台。它是由PortSwigger公司开发的,是渗透测试人员、安全研究人员和Web开发人员检查和分析Web应用程序安全问题的重要工具。它提供了一个直观的图形化界......
  • 了解AQS(AbstractQueuedSynchronizer)
    AQS(AbstractQueuedSynchronizer)是Java并发包中的一个核心同步器框架,它定义了一套多线程访问共享资源的同步机制。一、AQS的定义AQS,全称AbstractQueuedSynchronizer,即抽象队列同步器,是Java中的一个抽象类。它是构建锁或者其他同步组件的基础框架,通过继承AQS,子类可以实现自己的......
  • 深度学习-50-AI应用实战之基于mediapipe的手势识别
    文章目录1手势识别1.1手势识别技术1.2手势识别应用场景1.3手势识别基本原理2应用mediapipe2.1加载模型2.2处理图片2.2.1手势识别2.2.2人脸检测2.2.3姿态估计2.2.4表情识别2.3处理摄像头3参考附录1手势识别手势识别技术是一......
  • 【JUC】ConcurrentHashMap之computeIfAbsent
    ConcurrentHashMap之computeIfAbsentConcurrentHashMap的锁粒度更细publicclassTGestWordCount{publicstaticvoidmain(String[]args){demo(()->newConcurrentHashMap<String,LongAdder>(),(map,words)->......
  • JUC并发编程
    JUC并发编程文章目录JUC并发编程1.JUC线程池2.Fork/Join分支合并框架3.CompletableFuture异步回调1.JUC线程池概述和架构通过线程池可以创建线程线程池就是控制多个线程,将要执行的任务放到任务队列中,然后找空闲的线程去执行这些任务,如果线程数量超过了最大数......