当前位置: 首页 > news >正文

AQS-ReentrantLock

一、AQS

image.png
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具,也是 Lock 用来实现线程同步的核心组件。

1.AQS 的两种功能

独占和共享。

  • 独占锁:每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁。
  • 共享锁 ,允许多个线程同时获取锁 ,并发访问共享资源 ,比如ReentrantReadWriteLock

2.AQS的实现

AQS.png
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。
每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 AQS 队列中去; 当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

Node组成:

static final class Node {// 排他锁的标识static final Node EXCLUSIVE = null;// 如果带有这个标识,证明是失效了static final int CANCELLED =  1;// 具有这个标识,说明后继节点需要被唤醒static final int SIGNAL = -1;// Node对象存储标识的地方volatile int waitStatus;// 指向上一个节点volatile Node prev;// 指向下一个节点volatile Node next;// 当前Node绑定的线程volatile Thread thread;// 存储在Condition队列中的后继节点Node nextWaiter;// 返回前驱节点,如果前驱节点为null,抛出NPEfinal Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}// 将线程构造成一个Node,添加到等待队列Node(Thread thread, Node mode) {     // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}// 在Condition队列中使用Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}
}

3.AQS添加线程

添加节点.png

    1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己
    1. 通过 CAS 将 tail 重新指向新的尾部节点

4.AQS释放锁

释放锁.png
head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点:

    1. 修改 head 节点指向下一个获得锁的节点
    1. 新的获得锁的节点,将 prev 的指针指向 null

设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可。

二、CAS

1.CAS 的实现原理

protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

通过 cas 乐观锁的方式来做比较并替换,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false。这个操作是原子的,不会出现线程安全问题。

2.state属性

private volatile int state;
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样, 对于重入
锁的实现来说,表示一个同步状态。它有两个含义的表示

  • 当 state=0 时,表示无锁状态
  • 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候, state 会递增,比如重入 5 次,那么 state=5。 而在释放锁的时候,同样需要释放 5 次直到 state=0其他线程才有资格获得锁

3.Unsafe类

Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、
Hadoop、 Kafka 等;

Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、 CAS、线程同步、内存屏障

而 CAS 就是 Unsafe 类中提供的一个原子操作:
public final native boolean compareAndSwapInt(Object obj, long stateOffset, int expect, int update);

  • obj:需要改变的对象
  • stateOffset:偏移量(即之前求出来的 headOffset 的值)
  • expect:期待的值
  • update:更新后的值

整个方法的作用是如果当前时刻的值等于预期值 expect 相等,则更新为新的期望值 update,如果更新成功,则返回 true,否则返回false;

stateOffset:
一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置
所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量

compareAndSwapInt
unsafe.cpp 文件中compareAndSwarpInt 的实现:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset,jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj); //将 Java 对象解析成 JVM 的 oop(普通对象指针) 
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //根据对象 p 和地址偏移量找到地址
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于 cas 比较并替换, x 表示需要更新的值, addr 表示 state在内存中的地址, e 表示预期值
UNSAFE_END

三、ReentrantLock

ReentrantLock时序图:

ReentrantLockNonfairSyncSyncAbstractQueuedSynchronizerlock()lock()acquire()tryAcquire()nonfairTryAcquire()true/falseaddWaiter()ReentrantLockNonfairSyncSyncAbstractQueuedSynchronizer

1.lock()

public void lock() {// sync分为了公平和非公平sync.lock();
}

sync是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。
AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒, 但它并不具备业务功能, 所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。

Sync 有两个具体的实现类:

  • NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
  • FailSync: 表示所有线程严格按照 FIFO 来获取锁

NonfairSync#lock():

final void lock() {// 通过CAS的方式尝试将state从0修改为1,如果返回true,代表修改成功,如果修改失败,返回falseif (compareAndSetState(0, 1))// 将一个属性设置为当前线程,这个属性是AQS的父类提供的setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
  1. 非公平锁和公平锁最大的区别在于,非公平锁中抢占锁的逻辑是,不管有没有线程排队,直接cas去抢占
  2. CAS 成功,就表示成功获得了锁
  3. CAS 失败,调用 acquire(1)走锁竞争逻辑

2.AQS#acquire()

acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时 acquire(1)操作

public final void acquire(int arg) {// tryAcquire再次尝试获取锁资源,如果尝试成功,返回trueif (!tryAcquire(arg) &&// 获取锁资源失败后,需要将当前线程封装成一个Node,追加到AQS的队列中acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 线程中断selfInterrupt();
}
  • 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
  • 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
  • acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁

3.NonfairSync#tryAcquire()

方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false。
它是重写 AQS 类中的 tryAcquire 方法。

protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取AQS的state的值int c = getState();// 如果state为0,表示无锁状态,尝试再次获取锁资源if (c == 0) {// CAS尝试修改state,从0-1,如果成功,设置ExclusiveOwnerThread属性为当前线程if (compareAndSetState(0, acquires)) {// 保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁setExclusiveOwnerThread(current);return true;}}// 当前占有锁资源的线程是否是当前线程else if (current == getExclusiveOwnerThread()) {// 将state + 1int nextc = c + acquires;// 如果加1后,小于0,超所锁可重入的最大值,抛出Errorif (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 没问题,就重新对state进行复制setState(nextc);// 锁重入成功return true;}return false;
}

4.AQS#addWaiter()

当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node.
入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能

  • 1.将当前线程封装成 Node
  • 2.当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列
  • 3.如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列
// 说明前面获取锁资源失败,放到队列中等待
private Node addWaiter(Node mode) {// 创建Node类,并且设置thread为当前线程,设置为排它锁Node node = new Node(Thread.currentThread(), mode);// 获取AQS中队列的尾部节点,默认是 nullNode pred = tail;// 如果tail != null,说明队列中存在节点if (pred != null) {// 把当前线程的 Node 的 prev 指向 tailnode.prev = pred;// 通过 cas 把 node加入到 AQS 队列,也就是设置为 tailif (compareAndSetTail(pred, node)) {// 设置成功以后,把原 tail 节点的 next指向当前 nodepred.next = node;return node;}}// tail=null,把 node 添加到同步队列enq(node);return node;
}// enq :通过自旋操作把当前节点加入到队列中
// 队列没有节点,我是第一个, 如果前面CAS失败,也会进到这个位置重新往队尾进入。
private Node enq(final Node node) {// 死循环for (;;) {// 重新获取当前的tail节点为tNode t = tail;if (t == null) { // 队列没有节点, 我是第一个,没头没尾,都是空if (compareAndSetHead(new Node()))	// 初始化一个Node作为head,而这个head没有意义。// 将头尾都指向了这个初始化的Nodetail = head;} else {// 有节点,往队尾入// 当前节点的上一个指向tailnode.prev = t;// 基于CAS的方式,将tail节点设置为当前节点if (compareAndSetTail(t, node)) {   // 将之前的为节点的next,设置为当前节点t.next = node;return t;}}}
}

5.AQS#acquireQueued()

通过 addWaiter 方法把线程添加到链表后, 会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁:

  • 1.获取当前节点的 prev 节点
  • 2.如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
  • 3.抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
  • 4.如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
  • 5.通过 cancelAcquire 取消获得锁的操作
// 已经将node加入到了双向队列中,然后执行当前方法
final boolean acquireQueued(final Node node, int arg) {// 标识boolean failed = true;try {// 标识boolean interrupted = false;for (;;) {// 获取当前节点的上一个节点pfinal Node p = node.predecessor();// 如果p是头,说明有资格去争抢锁,尝试获取锁资源(state从0-1,锁重入操作),成功返回true,失败返回falseif (p == head && tryAcquire(arg)) {// 获取锁成功,设置head节点为当前节点,将thread,prev设置为null,因为拿到锁资源了 ;setHead(node);p.next = null;   // 把原 head 节点从链表中移除,帮助GC回收failed = false;  // 将标识修改为falsereturn interrupted;  // 返回interrupted }// 保证上一个节点是-1,才会返回true,才会将线程阻塞,等待唤醒获取锁资源if (shouldParkAfterFailedAcquire(p, node) &&// 基于Unsafe类的park方法,挂起线程parkAndCheckInterrupt();   // 针对fail属性,这里是唯一可能出现异常的地方,JVM内部出现问题时,可以这么理解,fianlly代码块中的内容,执行的几率约等于0interrupted = true; // 返回当前线程在等待过程中有没有中断过}} finally {if (failed)cancelAcquire(node);}
}

shouldParkAfterFailedAcquire
如果 ThreadA 的锁还没有释放的情况下, ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用 shouldParkAfterFailedAcquire 方法
Node 有 5 中状态

  • CANCELLED(1) :在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状态后的结点将不会再变化
  • SIGNAL(-1) :只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
  • CONDITION(-2)
  • PROPAGATE(-3):享模式下, PROPAGATE 状态的线程处于可运行状态
  • 默认状态(0)
    通过 Node 的状态来判断, ThreadA 竞争锁失败以后是否应该被挂起。
  1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把 CANCELLED 状态的节点移除
  3. 修改 pred 节点的状态为 SIGNAL,返回 false.
    返回 false 时,也就是不需要挂起,返回 true,则需要调用parkAndCheckInterrupt
    挂起当前线程
// node是当前节点,pred是上一个节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 获取上一个节点的状态int ws = pred.waitStatus;// 如果上一个节点状态为SIGNAL,意味着只需要等待其他前置节点的线程被释放if (ws == Node.SIGNAL)return true; // 返回 true,意味着可以直接放心的挂起了// ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点if (ws > 0) {do {// 将当前节点的prev指针指向了上一个的上一个node.prev = pred = pred.prev;} while (pred.waitStatus > 0);  // 一直找到小于等于0的,从双向列表中移除 CANCELLED 的节点// 将重新标识好的最近的有效节点的nextpred.next = node;} else {// 小于等于0,不等于-1,将上一个有效节点状态修改为-1compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}

parkAndCheckInterrupt:

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}

使用 LockSupport.park 挂起当前线程变成 WATING 状态
Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。 如果返回 true,意味着在 acquire 方法中会执行 selfInterrupt()。

selfInterrupt:

static void selfInterrupt() {Thread.currentThread().interrupt();
}

标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的。

cancelAcquire

// cancelAcquire方法
private void cancelAcquire(Node node) {// 如果当前节点为null,结束,健壮性判断if (node == null)return;// node不为null的前提下执行// 将当前node的线程置位null  , 竞争锁资源跟我没有关系了,node.thread = null;// 获取当前节点的前驱节点Node pred = node.prev;// 前驱节点的状态 > 0while (pred.waitStatus > 0)// 找到前驱中最近的非失效节点node.prev = pred = pred.prev;// 将第一个不是失效节点的后继节点声明出来Node predNext = pred.next;// 将当前节点置位失效节点。给别的Node看的。node.waitStatus = Node.CANCELLED;// 如果当前节点是尾节点,将尾节点设置为最近的有效节点(如果当前节点为尾节点的操作)if (node == tail && compareAndSetTail(node, pred)) {// 用CAS方式将尾节点的next设置nullcompareAndSetNext(pred, predNext, null);} else {int ws;// 中间节点操作// 如果上一个节点不是头节点if (pred != head &&获取上一届点状态,是不是有效((ws = pred.waitStatus) == Node.SIGNAL ||  // pred需要唤醒后继节点的(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {Node next = node.next;if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);  // 尝试将pred的前驱节点的next指向当前节点的next(必须是有效的next节点)} else {// 头结点,唤醒后继节点unparkSuccessor(node);}node.next = node; // help GC}
}

相关文章:

AQS-ReentrantLock

一、AQS 在 Lock 中&#xff0c;用到了一个同步队列 AQS&#xff0c;全称 AbstractQueuedSynchronizer&#xff0c;它是一个同步工具&#xff0c;也是 Lock 用来实现线程同步的核心组件。 1.AQS 的两种功能 独占和共享。 独占锁&#xff1a;每次只能有一个线程持有锁&#x…...

SpringCloud+Dubbo3 = 王炸 !

前言 全链路异步化的大趋势来了 随着业务的发展&#xff0c;微服务应用的流量越来越大&#xff0c;使用到的资源也越来越多。 在微服务架构下&#xff0c;大量的应用都是 SpringCloud 分布式架构&#xff0c;这种架构总体上是全链路同步模式。 全链路同步模式不仅造成了资源…...

机器学习主要内容的思维导图

机器学习 机器学习&#xff1a; 定义&#xff1a;能够从经验中学习从而能够 把事情不断做好的计算机程序 人工智能的一个分支和 实现方式 理论基础&#xff1a;概率论 数理统计 线性代数 数学分析 数值逼近 最优化理论 计算复杂理论 核心要素&#xff1a;数据 算法 模型 机器…...

嵌套走马灯Carousel

Carousel 的应用很广泛&#xff0c;基础用法这里不多做阐述&#xff0c;感兴趣的可以去element-gui了解Carousel 组件。 今天主要是梳理嵌套走马灯的逻辑&#xff0c;背景如下&#xff1a; 需要对项目做一个展示&#xff0c;项目可能有一个或多个&#xff0c;同时一个项目可能…...

实战——缓存的使用

文章目录前言概述实践一、缓存数据一致1.更新缓存类2.删除缓存类二、项目实践&#xff08;商城项目&#xff09;缓存预热双缓存机制前言 对于我们日常开发的应用系统。由于MySQL等关系型数据库读写的并发量是有一定的上线的&#xff0c;当请求量过大时候那数据库的压力一定会上…...

2023年中职网络安全竞赛跨站脚本渗透解析-2(超详细)

跨站脚本渗透 任务环境说明:需求环境可私信博主! 服务器场景:Server2126(关闭链接)服务器场景操作系统:未知访问服务器网站目录1,根据页面信息完成条件,将获取到弹框信息作为flag提交;访问服务器网站目录2,根据页面信息完成条件,将获取到弹框信息作为flag提交;访问…...

Scala的简单使用

文章目录Scala的简单使用&#xff08;一&#xff09;交互模式1、命令行方式2、文件方式&#xff08;二&#xff09;编译模式1、创建源程序2、编译成字节码3、解释执行对象Scala的简单使用 Scala可以在交互模式和编译模式两种方式下运行 &#xff08;一&#xff09;交互模式 在…...

Java之前缀和算法

目录 一.前缀和 1.前缀和介绍 2.编程中的前缀和 二.一维数组的动态和 1.题目描述 2.问题分析 3.代码实现 三.除自身以外数组的乘积 1.题目描述 2.问题分析 3.代码实现 四.和为 K 的子数组 1.题目描述 2.问题分析 3.代码实现 五.形成两个异或相等数组的三元组数目…...

基于GIS计算降雨侵蚀力R因子

一、数据来源介绍 &#xff08;一&#xff09;行政边界数据 本文所用到的河北唐山行政边界数据来源于中国科学院资源环境科学与数据中心&#xff08;https://www.resdc.cn/Default.aspx&#xff09;。 &#xff08;二&#xff09;降水量数据 本文所用到的降水量数据来源于国家…...

大数据时代下的企业网络安全

在大数据技术迅猛发展的今天&#xff0c;网络安全问题已经发展成一个广受关注的热门研究方向。有人说&#xff0c;“大数据下&#xff0c;人人裸奔”&#xff0c;隐私保护、数据防护日益成为广大学者、企业研究的焦点。 面对这种安全威胁&#xff0c;企业必须实施一些有效的信…...

【跟我一起读《视觉惯性SLAM理论与源码解析》】第三章第四章 SLAM中常用的数学基础知识相机成像模型

齐次坐标能大大简化在三维空间中点、线、面表达方式和旋转、平移等操作在齐次坐标下&#xff0c;两个点的叉积结果可以表示一条直线l;也可以用两条直线的叉积结果表示它们的齐次坐标交点&#xff0c;关于叉积其实十四讲解释的还是比较清楚的&#xff0c;和李代数李群的关系可以…...

LeetCode 242. 有效的字母异位词

242. 有效的字母异位词 难度&#xff1a;easy\color{Green}{easy}easy 题目描述 给定两个字符串 sss 和 ttt &#xff0c;编写一个函数来判断 ttt 是否是 sss 的字母异位词。 注意&#xff1a; 若 sss 和 ttt 中每个字符出现的次数都相同&#xff0c;则称 sss 和 ttt 互为字…...

力扣mysql刷题记录

mysql刷题记录 刷题链接https://leetcode.cn/study-plan/sql/?progressjkih0qc mysql冲&#xff01;mysql刷题记录1699. 两人之间的通话次数1251. 平均售价1571. 仓库经理1445. 苹果和桔子1193. 每月交易 I1633. 各赛事的用户注册率1173. 即时食物配送 I1211. 查询结果的质量…...

Linux基础命令-lsof查看进程打开的文件

Linux基础命令-uptime查看系统负载 Linux基础命令-top实时显示系统状态 Linux基础命令-ps查看进程状态 文件目录 前言 一 命令的介绍 二 语法及参数 2.1 使用help查看命令的语法信息 2.2 常用参数 2.2.lsof命令-i参数的条件 三 命令显示内容的含义 3.1 FD 文件描述符的…...

常用电平标准

现在常用的电平标准有TTL CMOS LVTTL LVCMOS LVDS PCI等&#xff0c;下面简单介绍一下各自的供电电源、电平标准及注意事项数字电路中&#xff0c;由TTL电子元件组成电路使用的电平。电平是个电压范围。标准输出高电平(VOH): 2.4V标准输出低电平(VOL)&#xff1a;0.4V通常输出高…...

小程序开发注意点

1.组件样式隔离注意点 2.methods方法 3.自定义组件的properties参数 4.自定义组件的事件监听 5.纯数据字段 6.插槽 单个插槽 启用多插槽 使用多个插槽 7.属性绑定实现父传子功能 例如在这里有一个组件为<one></one>&#xff0c;那么可以在组件当中传入参数 &l…...

自行车出口欧盟CE认证,新版自行车标准ISO 4210:2023与ISO 8098:2023发布

2023年1月&#xff0c;国际标准化组织ISO发布了新版“自行车以及儿童自行车的测试标准”&#xff0c;即ISO 4210&#xff1a;2023以及ISO 8098:2023&#xff0c;用于取代了SO 4210&#xff1a;2015以及ISO 8098:2015。新版标准一经发布&#xff0c;立即生效。欧盟标准化委员会C…...

2020蓝桥杯真题回文日期 C语言/C++

题目描述 2020 年春节期间&#xff0c;有一个特殊的日期引起了大家的注意&#xff1a;2020 年 2 月 2 日。因为如果将这个日期按 “yyyymmdd” 的格式写成一个 8 位数是 20200202&#xff0c;恰好是一个回文数。我们称这样的日期是回文日期。 有人表示 20200202 是 “千年一遇…...

postman入门到精通之【接口知识准备】(一)

postman入门到精通之【接口知识准备】&#xff08;一&#xff09; 目录&#xff1a;导读 前言 接口测试概念 接口测试 接口测试的原理 常用接口测试工具 接口测试基础知识 接口的定义 接口的分类 HTTP接口 Web Service接口 RESTful接口 HTTP请求 统一资源定位符&…...

【算法数据结构体系篇class07】:加强堆

一、手动改写堆&#xff08;非常重要&#xff09;&#xff01;系统提供的堆无法做到的事情&#xff1a;1&#xff09;已经入堆的元素&#xff0c;如果参与排序的指标方法变化&#xff0c;系统提供的堆无法做到时间复杂度O(logN)调整&#xff01;都是O(N)的调整&#xff01;2&am…...

Taro3.x 容易踩坑的点(阻止滚动穿透,弹框蒙层父级定位)

解决弹框滚动的时候&#xff0c;下层也会滚动问题》阻止滚动穿透(react,vue)案例描述&#xff1a;页面展示时需要滚动条才可以显示完整&#xff0c;但是当我们显示弹框的时候&#xff0c;即使不需要滚动条&#xff0c;但是页面仍然可以滚动&#xff0c;并且下层内容会随着滚动变…...

SpringBoot+ActiveMQ-发布订阅模式(消费端)

ActiveMQ消息中间件的发布订阅模式 主题 topictopic生产端案例(配合topic消费端测试)&#xff1a;SpringBootActiveMQ Topic 生产端ActiveMQ版本&#xff1a;apache-activemq-5.16.5案例源码:SpringBootActiveMQ-发布订阅DemoSpringBoot集成ActiveMQ Topic消费端的pom.xml<?…...

vscode下使用arduino插件开发ESP32 Heltec WiFi_Kit_32_V3

下载vsCode 添加 arduino 插件 在Arduino IDE 中添加开发板&#xff0c;注意只能用右侧的开发板管理器添加&#xff0c;自己下载之后复制进去的IDE认&#xff0c;但是vsCode不认&#xff0c;搜索ESP32 第一个库里面只有到V2的&#xff0c;没有V3&#xff0c;要安装下面那个 H…...

吐血整理AutoSAR Com-Stack 的配置【基于ETAS】

总目录链接>> AutoSAR入门和实战系列总目录 文章目录01.软件组件和系统说明02.基本软件配置03.系统数据映射04.代码生成05.代码整合06.测试下图显示了基于 AUTOSAR 的 ECU SW 的结构。纵观BSW&#xff0c;大体分为三层。三层模块中&#xff0c;与通信相关的模块称为通信…...

面向对象进阶之元类

6. 元类 Python 中一切皆对象&#xff0c;对象是由类实例化产生的。那么类应该也有个类去产生它&#xff0c;利用 type() 函数我们可以去查看&#xff1a; class A:pass a1 A() print(type(a1)) print(type(A))<class __main__.A> <class type>由上可知&#xf…...

【Android AIDL之详细使用】

Android AIDL之详细使用一级目录概述使用场景语法相关编码实践服务端&#xff1a;java文件修改AndroidManifest客户端坑一级目录 概述 AIDL叫Android接口定义语言&#xff0c;是用于辅助开发者完成Android跨进程编程的工具。 从某种意义上说AIDL其实是一个模板&#xff0c;因…...

ASP.NET MVC | 简介

目录 前提 1.教程 2.MVC 编程模式 最后 前提 在学习学过很多课程&#xff0c;但是最主要学的还是ASP.NET MVC这门课程&#xff0c;工作也是用的ASP.NET MVC&#xff0c;所以写一点ASP.NET MVC的东西&#xff0c;大家可以来看看&#xff0c;我自己不会的时候也不用找别的地方…...

95后刚毕业2、3年就年薪50W,才发现,打败我们的不是年龄····

一刷朋友圈&#xff0c;一读公众号&#xff0c;一打开微博&#xff0c;甚至是一和朋友聊天&#xff0c;这些让人焦虑的话题总会铺天盖地的袭来&#xff1a; Ta刚毕业半年&#xff0c;就升职加薪当上了测试主管 &#xff08;同样是一天24小时&#xff0c;为什么同龄人正在抛弃…...

动态分析和静态分析最主要的区别是什么?

动态分析和静态分析主要的区别是什么&#xff1f; 动态分析和静态分析的主要区别是是否考虑时间因素。 动态分析&#xff08;dynamic analysis&#xff09;是相对于静态分析来讲的&#xff0c;动态分析是只改变一下自变量&#xff0c;因变量相应的做出的改变&#xff0c;动态改…...

WebUI 学习笔记

WebUI 学习笔记 背景此插件主要用于在数字孪生方向做 UI 显示的效果。比如一些温度曲线需要显示出来,可以直接用插件,配合html 文件,直接显示出来。 准备工作我们采用4.27 版本进行开发;...