初识 AQS

一、什么是 AQS

AQS是一个用来构建锁和同步器的框架。JUC 的同步器底层都是用了 AQS,例如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier,ReentrantReadWriteLock。

二、前置知识

在了解 AQS之前,大家需要先去了解一下LockSupport,可以参考这篇文章JUC之LockSupport

三、AQS 的核心思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中

 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

四、源码解析

使用方法很简单,线程操纵资源类就行。主要方法有两个lock() 和unlock().我们深入代码去理解。我在源码的基础上加注释,希望大家也跟着调试源码。其实非常简单。

4.1、AQS 的数据结构

AQS 主要有三大属性分别是 head ,tail, state,其中state 表示同步状态,head为等待队列的头结点,tail 指向队列的尾节点。
    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;
class Node{
  //节点等待状态
  volatile int waitStatus;
  // 双向链表当前节点前节点
  volatile Node prev;
  // 下一个节点
  volatile Node next;
  // 当前节点存放的线程
  volatile Thread thread;
  // condition条件等待的下一个节点
  Node nextWaiter;
}

waitStatus 只有特定的几个常量,相应的值解释如下:

以ReentranLock的非公平锁为例。我们主要关注的方法是lock(),和unlock()。

4.2、lock源码分析

首先看一下lock()方法源代码,直接进入非公平锁的lock方法:

        final void lock() {
            //1、判断当前state 状态, 没有锁则当前线程抢占锁
            if (compareAndSetState(0, 1))
                // 独占锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 2、锁被人占了,尝试获取锁,关键方法了
                acquire(1);
        }

进入 AQS的acquire() 方法:

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

lock方法主要由tryAquire()尝试获取锁,addWaiter(Node.EXCLUSIVE) 加入等待队列,acquireQueued(node,arg)等待队列尝试获取锁。


4.3、tryAquire 方法源码

既然是非公平锁,那么我们一进来就想着去抢锁,不管三七二一,直接试试能不能抢到,抢不到再进队列。

  final boolean nonfairTryAcquire(int acquires) {
            //1、获取当前线程
            final Thread current = Thread.currentThread();
            // 2、获取当前锁的状态,0 表示没有被线程占有,>0 表示锁被别的线程占有
            int c = getState();
            // 3、如果锁没有被线程占有
            if (c == 0) {
                 // 3.1、 使用CAS去获取锁,   为什么用case呢,防止在获取c之后 c的状态被修改了,保证原子性
                if (compareAndSetState(0, acquires)) {
                    // 3.2、设置独占锁
                    setExclusiveOwnerThread(current);
                    // 3.3、当前线程获取到锁后,直接发挥true
                    return true;
                }
            }
            // 4、判断当前占有锁的线程是不是自己
            else if (current == getExclusiveOwnerThread()) {
                // 4.1 可重入锁,加+1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                 // 4.2 设置锁的状态
                setState(nextc);
                return true;
            }
            return false;
        }

4.4、addWaiter()方法的解析

private Node addWaiter(Node mode),当前线程没有货得锁的情况下,进入CLH队列。

 private Node addWaiter(Node mode) {
 		// 1、初始化当前线程节点,虚拟节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 2、获取尾节点,初始进入节点是null
        Node pred = tail;
        // 3、如果尾节点不为null,怎将当前线程节点放到队列尾部,并返回当前节点
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果尾节点为null(其实是链表没有初始化),怎进入enq方法
        enq(node);
        return node;
    }
    
   // 这个方法可以认为是初始化链表
   private Node enq(final Node node) {
   		// 1、入队 : 为什么要用循环呢?  
        for (;;) {
           // 获取尾节点
            Node t = tail;
           // 2、尾节点为null
            if (t == null) { // Must initialize
               // 2.1 初始话头结点和尾节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } 
            // 3、将当前节点加入链表尾部
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

有人想明白为什么enq要用for(;;)吗? 咋一看最多只要循环2次啊! 答疑来了,这是对于单线程来说确实是这样的,但是对于多线程来说,有可能在第2部完成之后就被别的线程先执行入链表了,这时候第3步cas之后发现不成功了,怎么办?只能再一次循环去尝试加入链表,直到成功为止。

4.5、acquireQueued()方法详解

addWaiter 方法我们已经将没有获取锁的线程放在了等待链表中,但是这些线程并没有处于等待状态。acquireQueued的作用就是将线程设置为等待状态。

 final boolean acquireQueued(final Node node, int arg) {
         // 失败标识
        boolean failed = true;
        try {
            // 中断标识
            boolean interrupted = false;
            for (;;) {
                // 获取当前节点的前一个节点
                final Node p = node.predecessor();
                // 1、如果前节点是头结点,那么去尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    // 重置头结点
                    setHead(node);
                    p.next = null; // help GC
                    // 获得锁
                    failed = false;
                    // 返回false,节点获得锁,,,然后现在只有自己一个线程了这个时候就会自己唤醒自己
                    // 使用的是acquire中的selfInterrupt(); 
                    return interrupted;
                }
                // 2、如果线程没有获得锁,且节点waitStatus=0,shouldParkAfterFailedAcquire并将节点的waitStatus赋值为-1
                //parkAndCheckInterrupt将线程park,进入等待模式,
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        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.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

4.6、unlock源码分析

unlock释放锁。主要利用的是LockSupport

   public final boolean release(int arg) {
         // 如果成功释放独占锁,
        if (tryRelease(arg)) {
            Node h = head;
            // 如果头结点不为null,且后续有入队结点
            if (h != null && h.waitStatus != 0)
                //释放当前线程,并激活等待队里的第一个有效节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // 如果释放锁着返回true,否者返回false
    // 并且将sate 设置为0
     protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }


      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;
        if (ws < 0)
            // 重置头结点的状态waitStatus
            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;
        // s.waitStatus > 0 为取消状态 ,结点为空且被取消
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 获取队列里没有cancel的最前面的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果节点s不为null,则获得锁
        if (s != null)
            LockSupport.unpark(s.thread);
    }

五、案例

public class AQSDemo {
    private static int num;

    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                        Thread.sleep(1000);
                        num += 1000;
                    System.out.println("A 线程执行了1秒,num = "+ num);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                finally {
                    lock.unlock();
                }
            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Thread.sleep(500);
                    num += 500;
                    System.out.println("B 线程执行了0.5秒,num = "+ num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                finally {
                    lock.unlock();
                }
            }
        },"B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    Thread.sleep(100);
                    num += 100;
                    System.out.println("C 线程执行了0.1秒,num = "+ num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                finally {
                    lock.unlock();
                }
            }
        },"C").start();
    }
}

当线程A 到lock()方法时,通过compareAndSetState(0,1)获得锁,并且获得独占锁。当B,C线程去争抢锁时,运行到acquire(1),C线程运行tryAcquire(1),接着运行nonfairTryAcquire(1)方法,未获取锁,最后返回false,运行addWaiter(),运行enq(node),初始化head节点,同时C进入队列;再进入acquireQueued(node,1)方法,初始化waitStatus= -1,自旋并park()进入等待。
接着B线程开始去抢锁,B线程运行tryAcquire(1),运行nonfairTryAcquire(1)方法,未获得锁最后返回false,运行addWaiter(),直接添加到队尾,同时B进入队列;在进入acquireQueued(node,1)方法,初始化waitStatus= -1,自旋并park()进入等待。

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

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

相关文章

C++ 01 之 hello world

c01helloworld.cpp #include <iostream>using namespace std;int main() {cout << "hello world" << endl;return 0; } #include<iostream>; 预编译指令&#xff0c;引入头文件iostream.using namespace std; 使用标准命名空间cout <&l…

LW-DETR:实时目标检测的Transformer, Apache-2.0 开源可商用,论文实验超 YOLOv8

LW-DETR&#xff1a;实时目标检测的Transformer&#xff0c; Apache-2.0 开源可商用&#xff0c;论文实验超 YOLOv8 LW-DETR 架构实例化高效训练高效推理 目的与解法拆解ViT编码器和DETR解码器多级特征图聚合变形交叉注意力窗口注意力和全局注意力 论文&#xff1a;https://arx…

1124. 表现良好的最长时间段 (python) 前缀和 分类讨论 最大长度 力扣 面试题

给你一份工作时间表 hours&#xff0c;上面记录着某一位员工每天的工作小时数。 我们认为当员工一天中的工作小时数大于 8 小时的时候&#xff0c;那么这一天就是「劳累的一天」。 所谓「表现良好的时间段」&#xff0c;意味在这段时间内&#xff0c;「劳累的天数」是严格 大…

什么是 URL 过滤?是如何保障浏览体验的?

互联网是一个无边无际的空间&#xff0c;几乎包含了你能想象到的一切。不幸的是&#xff0c;这意味着也存在着从不合适到非常危险的网站。这就是 URL 过滤可以发挥作用的地方。 一、URL 过滤的含义 我们希望您已经熟悉 URL&#xff08;统一资源定位器&#xff09;&#xff0c;…

在韩国遇到阿姨叫“아줌마”还是“이모”?都不如称呼好!柯桥学韩语来银泰附近基础教学通俗易懂

认识母音 母音&#xff0c;又叫元音&#xff0c;共21个&#xff0c;包含10个基本母音和11复合母音&#xff08;又称双元音&#xff09;。 10个基本母音&#xff1a;ㅏ(a)、ㅑ(ya)、ㅓ(eo)、ㅕ(yeo)、ㅗ(o)、ㅛ(yo)、ㅜ(u)、ㅠ(yu)、ㅡ(eu)、ㅣ(i) 11个复合母音&#xff1a;ㅐ(a…

【ETAS CP AUTOSAR基础软件】BswM模块详解

文章包含了AUTOSAR基础软件&#xff08;BSW&#xff09;中BswM模块相关的内容详解。本文从AUTOSAR规范解析&#xff0c;ISOLAR-AB配置以及模块相关代码分析三个维度来帮读者清晰的认识和了解BswM这一基础软件模块。文中涉及的SOLAR-AB配置以及模块相关代码都是依托于ETAS提供的…

pdf添加书签的软件,分享3个实用的软件!

在数字化阅读日益盛行的今天&#xff0c;PDF文件已成为我们工作、学习和生活中不可或缺的一部分。然而&#xff0c;面对海量的PDF文件&#xff0c;如何高效地进行管理和阅读&#xff0c;成为了许多人关注的焦点。其中&#xff0c;添加书签功能作为提高PDF文件阅读体验的重要工具…

数据结构01 栈及其相关应用

栈是一种线性数据结构&#xff0c;栈的特征是数据的插入和删除只能通过一端来实现&#xff0c;这一端称为“栈顶”&#xff0c;相应的另一端称为“栈底”。 栈及其特点 用一个简单的例子来说&#xff0c;栈就像一个放乒乓球的圆筒&#xff0c;底部是封住的&#xff0c;如果你想…

c++线性关系求值

目的 线性关系是最简单的关系,但也是编程当中最常用的一种关系,很多行业,都用。 可以说,其是准确的,有时利用了正比例的关系,其具有预测性,检验其它数据是否正确,应用实在太多了。 生活中太多的东西可以认为成线性的,比如:年龄越大,经验越丰富,这也是线性关系,因…

揭秘湖北工程类助理工程师证书:纸质版 vs 电子版,哪个更靠谱

"揭秘湖北工程类助理工程师证书&#xff1a;纸质版 vs 电子版&#xff0c;哪个更靠谱&#xff1f;" 2024年湖北工程类助理工程师证书纸质版VS电子版 很多人会疑惑不是从2021年底就发布相关文件&#xff0c;湖北初级、中级、高级职称进入电子版证书时代&#xff0c;为…

分组聚集查询-GROUP BY子句

一、GROUP BY子句位置 SELECT 【ALL|DISTINCT】<目标列表达式1>【,<目标列表达式2>,...】 FROM <表名或视图名1>【&#xff0c;<表名或视图名2>&#xff0c;...】 【WHERE <元组选择条件表达式>】 【GROUP BY <属性列名1>【&#xff0…

2024 年 5 月公链研报:监管调整与市场新动向

作者&#xff1a;stellafootprint.network 数据来源&#xff1a;公链 Research 页面 五月份&#xff0c;加密货币市场经历了重要的监管和政治动态。美国证券交易委员会&#xff08;SEC&#xff09;批准了现货以太坊 ETF 的初步申请文件&#xff0c;这一举措提振了以太坊及其…

pom学习笔记:kimi的自动化操作

1.先看结构&#xff1a; 声明&#xff1a;我是初学&#xff0c;可能有不合理的地方。 2.Base层。 我是把原来一个kimi的自动问答的代码改过来。 分析&#xff1a;其实我是新手&#xff0c;因为我用的浏览器是固定的&#xff0c;也没有打算和别人用。所以浏览器层面年的全部写…

蓝牙芯片TD5322A,蓝牙5.1数传芯片介绍—拓达半导体

蓝牙芯片原厂&#xff0c;拓达芯片TD5322A是一颗支持蓝牙BLE和SPP的数传芯片&#xff0c;蓝牙5.1版本。芯片的优点是尺寸小(SOP-8封装&#xff09;&#xff0c;性能强&#xff0c;价格低&#xff0c;以及简单明了的透传和串口AT控制功能&#xff0c;大大降低了在其它电子产品中…

React 渲染流程分析

React 页面是由组件组成的&#xff0c;从根组件直到叶组件&#xff0c;内部的组件数通过 Fiber 来保存并触发并发更新。页面的展示分为两部分&#xff0c;首先是初始化&#xff0c;所有组件首次展示&#xff0c;都要进行渲染&#xff0c;之后是更新流程&#xff0c;也就是页面产…

团队知识管理首选:12款优秀开源Wiki系统推荐

文章介绍了12款好用的开源Wiki&#xff1a;PingCode、DokuWiki、MediaWiki、Tiki Wiki CMS Groupware、XWiki、BookStack、PMWiki、Foswiki、GitBook、Wiki.js、TiddlyWiki、Slite。以及对比了一款非开源但提供免费版本的Wiki工具&#xff0c;以供大家选择。 在企业知识管理和团…

Vue3+vite部署nginx的二级目录,使用hash模式

修改router访问路径 import { createRouter, createWebHashHistory } from vue-routerconst router createRouter({history: createWebHashHistory (/mall4pc-bbc/),routes: [XXX,] })配置package.json文件 "build:testTwo": "vite build --mode testing --ba…

python dropna怎么用

pandas的设计目标之一就是使得处理缺失数据的任务更加轻松些。pandas使用NaN作为缺失数据的标记。 使用dropna使得滤除缺失数据更加得心应手。 dropna常用参数&#xff1a; # DataFrame.dropna(axis0, howany, threshNone, subsetNone, inplaceFalse) 主要的2个参数&#xff…

运筹学基础与应用(简洁版总复习)

第一章 线性规划及单纯形法 图解法 单纯形法 大m法 看案例&#xff08;综合题&#xff09; 化标准形式 目标函数的转换 min z变为max z 变量的变换 变量取值无约束 约束方程的转换 ≤&#xff1a;加一个松弛变量 ≥&#xff1a;减一个剩余变量 变量符号≤0的变换 保持变量≥…

618家用智能投影仪推荐:这个高性价比品牌不容错过

随着科技的不断进步&#xff0c;家庭影院的概念已经从传统的大屏幕电视逐渐转向了更为灵活和便携的家用智能投影仪。随着618电商大促的到来&#xff0c;想要购买投影仪的用户们也开始摩拳擦掌了。本文将从投影仪的基础知识入手&#xff0c;为您推荐几款性价比很高的投影仪&…