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

「JVM 高效并发」Java 内存模型

Amdahl 定律代替摩尔定律成为了计算机性能发展的新源动力,也是人类压榨计算机运算能力的最有力武器;

  • 摩尔定律,描述处理器晶体管数量与运行效率之间的发展关系;
  • Amdahl 定律,描述系统并行化与串行化的比重与系统运算加速能力之间的关系;

让计算机同时做几件事情,可以在大量 I/O 等待中充分利用计算机的计算能力,避免计算资源处于等待其他资源的空闲状态;

TPS,每秒处理的事务数,是衡量一个服务性能高低的重要指标,这也与程序的并发能力有密切关系;

程序线程并发协调的有条不紊,效率才能最佳;线程间频繁争用数据,相互阻塞甚至死锁,则会大大降低程序并发能力;

硬件的效率与一致性

计算机的存储与处理器的运算速度存在几个数量级的差距,而计算过程不可能消除存储设备(内存交互读取运算数据存储运算结果等),为了尽可能给运算加速,计算机系统不得不加入一层或多层高速缓存(Cache)作为内存与处理器之间的缓冲;

请添加图片描述

  • 缓存一致性Cache Coherence),多路处理器每个处理器都有自己的高速缓存(共享内存多核系统,Shared Memory Multiprocessors System),它们又共享统一主内存(Main Memory),当多处理器的运算任务涉及统一主内存区域,可能导致各自缓存数据的不一致;为了解决不一致问题,需要定义一些协议,如 MSI、MESI(Illinois Protocal)、MOSI、Synapse、Firefly、Dragon Protocal 等;
  • 内存模型,在内存缓存一致性协议下,对特定内存或高速缓存进行读写访问的过程抽象;不同架构的物理机拥有不一样的内存模型,JVM 也有自己的内存模型;
  • 乱序执行Out-Of-Order Execution),为了使处理器内存的运算单元能力尽可能充分利用,处理器将输入的代码乱序执行,在将乱序执行的结果重组,但不保证各语句计算的先后顺序与代码中定义的顺序一致,这可能导致计算结果与顺序执行时不一致;
  • 指令重排Instruction Reorder),JVM 即时编译器中的乱序执行

文章目录

      • 1. 主内存与工作内存
      • 2. 内存间交互操作
      • 3. 对于 volatile 型变量的特殊规则
      • 4. 针对 long 和 double 型变量的特殊规则
      • 5. 原子性、可见性与有序性
      • 6. 先行发生原则

  • Java Memory Model,JMM,Java 内存模型,定义程序中各种变量(实例字段、静态字段、构成数组对象的元素等,不包含局部变量与方法参数等线程私有变量)的访问规则(如 JVM 把变量存储在内存、从内存中取出变量值的底层细节),试图屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各平台达到一致内存访问效果;(C/C++ 等主流程序语言直接使用物理硬件和操作系统的内存模型,导致一些场景在不同平台需要编写正对性的程序);JDK 2 建立,JDK 5 成熟并完善;

JMM 的定义需要让 JVM 的实现有足够自由地去利用硬件的各种特性(寄存器、高速缓存和指令集中某些特有的指令)来获取更好的执行速度;

1. 主内存与工作内存

请添加图片描述

  • 主内存Main Memory),与物理硬件的主内存类似,实际只是 JVM 内存的一部分;主要对应 Java Heap 中对象实例数据部分,对应了物理硬件的内存;
  • 工作内存Working Memory),与物理硬件的高速缓存类似,每条线程独占,保存了该线程使用的变量的主内存副本(仅对象中被线程访问到的字段,非整个对象),线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行(不能直接在主内存进行);线程间的变量值的传递必须通过主内存来进行;主要对应 Java VM Stack 中的部分区域;JVM 为了更好的运行速度,可能会让工作内存优先存储在寄存器和高速缓存中,因为程序运行时主要访问的是工作内存;

reference 类型引用的对象在 Java Heap 被各个线程共享,但 reference 本身是 Java Stack 的局部变量,是线程私有的;

volatile 变量的读写也必须经过工作内存的拷贝,但其有序性让其如同直接在主内存读写;

2. 内存间交互操作

主内存与工作内存交互协议的 8 种原子性操作

  • lock(锁定),把一个主内存的变量标识为某个线程独占状态;
  • unlock(解锁),把一个主内存的锁定状态的变量释放出来,释放之后的变量才可以被其他线程锁定;
  • read(读取),把一个主内存的变量传输到线程的工作内存中;
  • load(载入),把通过 read 操作传输到工作内存的变量放入工作内存的变量副本中;
  • use(使用),把工作内存中一个变量的值传递给执行引擎(JVM 需要使用变量的字节码指令时所需执行的操作);
  • assign(赋值),把一个从执行引擎接收的值赋给工作内存的变量(JVM 给变量赋值的字节码指令所需执行的操作);
  • store(存储),把一个工作内存的变量传递给主内存;
  • write(写入),把通过 store 操作传输到主内存的变量放入主内存的变量中;

JMM 只要求 read 和 load、store 和 write 是顺序成对执行,但中间可以插入其他指令(如: read a、read b、load a、load b);

JMM 基本操作的规则

  • 不允许 read 和 load、store 和 write 操作单独出现;不允许主内存变量传输到工作内存,但工作内存不接收,反之亦然;
  • 不允许一个线程丢弃它最近的 assign 操作;变量在工作内存中改变后,必须将改变同步会主内存;
  • 不允许一个线程无 assign 操作时无原因的将数据同步回主内存;
  • 新的变量必须在主内存中诞生,对一个变量实施 use、store 之前,必须先执行 assign、load 操作;
  • 一个变量在同一时刻只允许被一个线程 lock,但可以被同一线程多次 lock,多次 lock 后,需要执行相同次数的 unlock 操作,变量才会被解锁;
  • 不允许对一个没有被 lock 的变量执行 unlock,也不允许对其他线程 lock 的变量执行 unlock;
  • 对一个变量 unlock 之前,必须先把它同步回主内存(执行 store、write 操作);

double 和 long 类型的变量在一些平台的 load、store、read、write 操作允许拆分;

3. 对于 volatile 型变量的特殊规则

volatile 是 JVM 提供的最轻量的同步机制;JMM 为 volatile 变量定义了一些特殊的访问规则;

  • 可见性,JMM 保障 volatile 变量对所有线程可见,即当一个线程修改了这个变量,新值对其他所有线程立即可见(普通变量的传递需要通过主内存,A 线程回写新值,且 B 线程从主内存读取新值,新增对 B 线程才可见);
    • volatile 变量在各线程的工作内存中是不存在一致性问题的,但 Java 的运算操作并非原子操作,因此 volatile 变量的运算在并发下也不是安全的;
  • 禁止指令重排,普通变量仅保障在方法执行过程中所依赖赋值结果的地方得到正确的结果,不保障变量赋值操作的顺序与程序代码中的顺序一致;(线程内部表现为串行的语义、Within-Thread As-If-Serial Semantics);
/*** volatile 变量自增运算测试** @author Aurelius Shu* @since 2023-02-25*/
public class VolatileTest {public static volatile int race = 0;public static void increase() {race++;}private static final int THREADS_COUNT = 20;public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[THREADS_COUNT];for (int i = 0; i < THREADS_COUNT; i++) {threads[i] = new Thread(() -> {for (int i1 = 0; i1 < 10000; i1++) {increase();}});threads[i].start();}// 等待所有累加线程都结束for (Thread thread : threads) {thread.join();}System.out.println(race);}
}

执行结果并非预期的 200000,而是小于 200000,且每次都不一样;

Class 字节码

public static void increase();Code:Stack=2, Locals=0, Args_size=00: getstatic        #13; //Field race:I3: iconst_14: iadd5: putstatic        #13; //Field race:I8: returnLineNumberTable:line 14: 0line 15: 8

volatile 关键字保障了 getstatic 获取的 race 值是正确的,但在对栈顶的 race 执行 iconst_1、iadd 这些指令时,其他线程可能已经对 race 进行了更新,此时栈顶的 race 值就过期了,因此 putstatic 指令最终将较小的 race 值同步回了主内存;

一条字节码指令还可能被转化为若干本地机器指令,因此字节码指令也不是原子性操作;

通过 volatile 保障一致性的必要条件

  • 运算结果并不依赖变量的当前值,或者能够保障只有单一线程修改变量的值(一写多读);
  • 变量不需要与其他的状态变量共同参与不变约束;

volatile 使用场景示例

volatile boolean shutdownRequested;public void shutdown() {shutdownRequested = true;
}public void doWork() {while (!shutdownRequested) {// 代码的业务逻辑}
}

当 shutdown() 方法被调用时,可以保证所有线程中 doWork() 可以立即停止;

指令重排演示

Map configOptions;
char[] configText;
// 此变量必须定义为volatile
volatile boolean initialized = false;// 假设以下代码在线程 A 中执行
// 模拟读取配置信息,当读取完成后
// 将 initialized 设置为 true,通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;// 假设以下代码在线程 B 中执行
// 等待 initialized 为true,代表线程 A 已经把配置信息初始化完成
while (!initialized) {sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();

若未对 initalized 变量使用 volatile,则线程 A 中 initialized = true; 操作(机器码级别指令)可能被提前执行,从而导致线程 B 中也提前以为配置信息已就绪,导致 B 线程出错;

DCL 单例模式

public class Singleton {private volatile static Singleton instance;public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}public static void main(String[] args) {Singleton.getInstance();}
}

对 instance 变量赋值的汇编代码

0x01a3de0f: mov     $0x3375cdb0,%esi        ;...beb0cd75 33; {oop('Singleton')}
0x01a3de14: mov     %eax,0x150(%esi)        ;...89865001 0000
0x01a3de1a: shr     $0x9,%esi               ;...c1ee09
0x01a3de1d: movb    $0x0,0x1104800(%esi)    ;...c6860048 100100
0x01a3de24: lock addl $0x0,(%esp)           ;...f0830424 00;*putstatic instance; - Singleton::getInstance@24

voliatile 修饰的变量在赋值后(mov %eax,0x150(%esi)),还插入了一个 lock add1 $0x0,(%esp) 操作,这是一个内存屏障;

  • 内存屏障(Memory Barrier、Memory Fence),指令重排不能把后面的指令排序到内存屏障之前的位置;若存在多个处理器同时访问一块内存,且其中一个在观测另一个,就需要内存屏障来保证一致性;

  • lock add1 $0x0,(%esp) (把 ESP 寄存器的值加 0)操作将本处理的缓存写入内存,并引起别的处理器或内核无效化(Invalidate)其缓存,相当于对缓存中的变量做了一次 JMM 中的 store 和 write 操作,通过这一操作,可以让 volatile 变量对其他处理器可见;这也正是指令重排无法越过内存屏障的原因;

  • 指令重排,处理器允许将多条指令不安程序代码顺序分发给各相应电路单元进行处理,但必须保证指令依赖情况保障得到正确执行结果;

// 指令 1:A=A+10;
// 指令 2:A=A*2;
// 执行 3:B=B-3;
/// 指令 1 与 2 存在 A 变量的依赖关系,不能重排
/// 指令 3 与 指令 1、2 不存在变量依赖关系,可以发生重排

volatile 变量的读操作与普通变量几乎无差别,写操作则稍慢,因为需要再本地代码中插入许多内存屏障指令来保障处理器不发生乱序执行;

volatile 的同步机制的性能优于锁;在 volatile 与锁中选择的唯一依据是 volatile 的语义是否满足使用场景的需求;

假定 T 线程对 V 和 W 两个 volatile 变量进行 read、load、use、assign、store、write 操作,需满足如下规则;

  • 在工作内存中,每次使用 V 前必须先从主内存刷新最新值,用于保障看见其他线程对 V 的修改(执行 use 前,必须先执行 load;只有执行 use 前才能执行 load;use 和 load、read 是必须连续一起出现的);
  • 在工作内存中,每次修改 V 后必须立即同步回主内存中,用于保障其他线程可以看到自己对 V 的修改(执行 assign 后,必须执行 store;只有执行 assign 后才能执行 store;assign 和 store、write 是必须连续一起出现的);
  • volatile 修饰的变量不会被指令重排,用于保障代码的执行顺序与程序中的顺序相同(T 对 V 的 use 或 assign 先于 T 对 W 的 use 或 assign,则相应的 T 对 V 的 read 或 write 必须先于 T 对 W 的 read 或 write);

volatile 的指令重排屏蔽语义在 JDK 5 中被完全修复,此前是无法完全避免的,因此在 JDK 5 之前无法安全的使用 DCL 实现单例模式;

4. 针对 long 和 double 型变量的特殊规则

  • long 和 double 的非原子性协定Non-Atomic Treatment of double and long Variables),JMM 特别规定:运行 VM 将没有被 volatile 修饰的 64 位数据的读写操作划分为 2 次 32 位数据进行操作(允许 JVM 自行选择是否保证 64 位数据的 load、store、read、write 的原子性);

若多个线程共享一个非 volatile 的 long 或 double 变量,多个线程同时对他们进行读写操作,可能读取到一个半个变量的数值(一般只出现在 32 位 JVM;由于存在浮点运算器 Floating Point Unit,FPU,double 类型通常不会出现非原子性访问问题);

  • -XX:+AlwaysAtomicAccesses,JDK 9 起可以如此约束 JVM 对所有数据类型进行原子性访问;

实际开发中,除非数据有明确的线程争用,否则一般不需要因此而刻意将 long 和 double 变量声明为 volatile;

5. 原子性、可见性与有序性

JMM 是围绕着并发过程中如何处理原子性、可见性、有序性着三个特征来建立的;

  • 原子性Atomicity),JMM 直接保证 read、load、assign、use、store、write 这 6 个操作的原子性,可以大致认为基本数据类型的访问、读写是原子性的(long 和 double 存在非原子性协定,但基本可以忽略);

在实际应用中更大范围的原子性保障可以通过 JMM 提供的 lock、unlock 操作来实现;在字节码层面相应是 monitorenter、monitorexit 指令;在 Java 代码层次则是同步块 synchronized 关键字;

  • 可见性Visibility),当一个线程修改了共享变量的值,其他线程能够立即得知这个修改;

JMM 规定变量的修改必须通过主内存来同步;volatile 变量相比普通变量,会保证新值立即同步到主内存,以及每次使用前立即从主内存刷新;

synchronized 可以实现可见性,因为对一个变量执行 unlock 之前,必须先把变量同步回主内存(执行 store、write);

final 被 final 修饰的字段在构造器中被初始化后会立即被其他线程看见;

public static final int i;
public final int j;static {// 一经初始化其他线程可见i = 0;// ...
}{// 一经初始化其他线程可见j = 0;// ...
}
  • 有序性Ordering),机器码执行的顺序与程序中代码顺序是否一致;

  • 在本线程内观察,所有操作都是有序的(线程内似表现为串行Within-Thread As-If-Serial Semantics);

  • 在一个线程中观察另一线程,所有操作都是无序的(指令重排、工作内存与主内存同步延迟);

Java 语言通过 volatile 和 synchronized 两个关键字保障线程间操作的有序性;volatile 本身直接禁止指令重排,synchromized 则同一时间只允许一个线程对变量进行 lock 操作;

synchronized 虽同时可以满足原子性、可见性、有序性,但滥用可能导致性能受到急剧影响;

6. 先行发生原则

先行发生Happens-Before)原则,描述 JMM 中两个操作的偏序关系;Java 语言无须任何同步手段就能保障的一些天然先行发生关系如下;

  • 程序次序规则Program Order Rule),在一个线程内的控制流中,书写在前面的操作先行发生于书写在后面的操作;需注意控制流不是代码顺序,而是分支、循环等结构;
  • 管程锁定规则Monitor Lock Rule),一个 unlock 操作先行发生于后面对同一锁的 lock 操作;
  • volatile 变量规则Volatile Variable Rule),对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作;
  • 线程启动规则Thread Start Rule),Thread 对象的 start() 方法先行发生于此线程的每一个动作;
  • 线程终止规则Thread Termination Rule),线程中所有操作都先行发生于对此线程的终止检测(Thread:: join()、Thread::isAlive());
  • 线程中断规则Thread Interruption Rule),对线程 interrupt() 方法的调用先行发生于中断线程的代码检测到中断事件的发生(Thread::interupted() 可以检测中断是否发生);
  • 对象终结规则Finalizer Rule),一个对象的初始化完成先行发生于它 finalize() 方法的开始;
  • 传递性Transitivity),操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,则 A 先行发生于 C;

先行发生原则演示

// A 线程执行;
i = 1;
// B 线程执行;
j = i;
// C 线程执行;
i = 2;

若 A 先行于 B(C 不存在),可以保证 j 的值是 1;
若 A 先行于 B,C 和 B 没有先行关系,则 C 可能发生在 A 与 B 之间,此时 j 的值可能是 1 也可能是 2,因为 B 可能不会观察到 C 对 i 的修改;这时不具备并发安全;

private int value = 0;public void setValue(int vlaue) {this.value = value;
}public int getValue() {return value;
}

假设两个方法在多线程中执行:

  • 不在一个线程,不适用程序次序规则
  • 没有同步快,不适用管程锁定规则
  • value 没有被 volatile 修饰,不适用 volatile 变量规则
  • 与线程启动、终止、中断、对象终结等规则无关;
  • 不存在规则可以传递
    因此对 value 的这两个操作不是线程安全的;

修复线程安全的方案:

  • 通过对 getter/setter 方法加 synchronized 同步,实现管程锁定规则;
  • 给 value 添加 volatile 修饰,套用 volatile 变量规则(value 的更新不依赖 value 的原值,这里可以适用);
// 在同一线程中执行,int j=2 可能先辈处理器执行,这不影响先行发生原则的正确性
int i = 1;
int j = 2;

时间先后顺序与先行发生原则之间基本没有因果关系,我们衡量并发安全问题时不能受到时间顺序的干扰,一切以先行发生原则为准;


上一篇:「JVM 编译优化」Graal 编译器

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》

相关文章:

「JVM 高效并发」Java 内存模型

Amdahl 定律代替摩尔定律成为了计算机性能发展的新源动力&#xff0c;也是人类压榨计算机运算能力的最有力武器&#xff1b; 摩尔定律&#xff0c;描述处理器晶体管数量与运行效率之间的发展关系&#xff1b;Amdahl 定律&#xff0c;描述系统并行化与串行化的比重与系统运算加…...

C语言刷题(2)——“C”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰来复习一下之前所学过的内容噢&#xff0c;复习的方式&#xff0c;那当然是刷题啦&#xff0c;现在&#xff0c;就让我们进入C语言的世界吧 当然&#xff0c;题目还是来源于牛客网 完完全全零基础 编程语言初学训练营_在线编程题…...

第一个 Spring MVC 注解式开发案例(初学必看)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

openresty学习笔记

openresty 简介 openresty 是一个基于 nginx 与 lua 的高性能 web 平台&#xff0c;其内部 集成了大量精良的 lua 库、第三方模块以及大数的依赖项。用于 方便搭建能够处理超高并发、扩展性极高的动态 web 应用、 web 服务和动态网关。 openresty 通过汇聚各种设计精良的 ngi…...

微信小程序DAY3

文章目录一、页面导航1-1、声明式导航1-2、编程式导航1-3、声明式导航传参1-4、编程式导航传参1-5、获取导航传递的参数二、页面事件2-1、下拉刷新事件2-1-1、启用下拉刷新2-1-2、配置下拉刷新2-1-3、监听页面下拉刷新事件2-2、上拉触底事件2-2-1、事件触发2-2-1、事件配置三、…...

【CAN】手把手教你学习CAN总线(一)

CAN总线一、CAN总线概念二、CAN的差分信号三、CAN总线的通信协议1、 帧起始2、仲裁段3、控制段4、数据段5、CRC段6、ACK段7、帧结束四、CAN的位时序1、同步段&#xff08;SS&#xff09;2、传播时间段&#xff08;PTS&#xff09;3、相位缓冲段&#xff08;PBS&#xff09;4、再…...

JUC 体系的基石——AQS

—— AQS&#xff08;AbstractQueuedSynchronizer&#xff09; 概念 抽象队列同步器&#xff1b;volatile cas 机制实现的锁模板&#xff0c;保证了代码的同步性和可见性&#xff0c;而 AQS 封装了线程阻塞等待挂起&#xff0c;解锁唤醒其他线程的逻辑。AQS 子类只需要根据状…...

Qt中信号与槽的使用

Qt中信号与槽的使用 Qt当中一个重要的东西是信号和槽&#xff0c;它被用于对象之间的通信。 在Qt中&#xff0c;例如“点击按钮”这个事件就是发送信号的对象&#xff0c;接收信号的是某一个窗口&#xff0c;响应信号的是一个处理&#xff0c;可以是隐藏窗口或者是关闭窗口。…...

力扣-销售员

大家好&#xff0c;我是空空star&#xff0c;本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;607. 销售员二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.正确示范④提交SQL运行结果5.其他总结前言 …...

HTML综合案例练习

一、展示简历内容 可以首先看一下我们的效果&#xff0c;之后再思考怎么实现 总的来说&#xff0c;这个练习不算难。 这里关于这个简历的代码编写我们不说太多&#xff0c;只注意以下几个内容即可&#xff1a; 注意及时查看我们的代码是否符合预期&#xff0c;即一段一段测 …...

MySQL运维

目录 1、日志 1、错误日志 2、二进制日志 3、查询日志 4、慢查询日志 2、主从复制 搭建 1、主库配置 2、从库配置 3、分库分表 1、简介 ​编辑 1、垂直拆分 2、水平拆分 3、实现技术 2、MyCat 3、MyCat使用和配置 配置 4、MyCat分片 1、垂直拆分 2、水平拆分…...

【网络原理10】构造HTTP请求、HTTPS加密

目录 一、构造HTTP请求 ①使用form表单构造HTTP请求&#xff1a; form表单是如何提交的 form提交的缺点 ②基于ajax构造http请求 如何使用Jquery框架 二、HTTPS 运营商劫持 HTTP的加密版本&#xff1a;HTTPS ①对称加密&#xff1a;客户端和服务端使用同一把密钥&…...

Allegro如何锁定报表界面操作指导

Allegro如何锁定报表界面操作指导 用Allegro做PCB设计的时候,进行测量的时候,比如测量器件两个PIN中间的间距,如下图,会有一个报表显示 但是当运行下一个命令的时候,报表会被自动关闭掉。 但是有时我们需要报表界面仍被保留 下面介绍如何将报表界面进行锁定,不受下一个…...

基于STM32的微型电子琴设计

基于STM32的微型电子琴设计报告中的图片和文字太多了&#xff0c;全部一个一个把搬过来太麻烦了,需要完整文本和代码自行q我963160156 第一章 总体设计1.1 系统功能1.2 主要技术性能指标第二章硬件设计2.1 整体硬件图2.2 按键模块2.3 扬声器模块2.4 显示模块2.5 主控模块第三章…...

Shell输入输出重定向

一、文件描述符 文件描述符是一个非负整数。它是一个索引值&#xff0c;指向进程打开的文件。 Linux 程序在执行任何形式的 I/O 操作时&#xff0c;都是在读取或者写入一个文件描述符。 每个文件描述符会与一个打开的文件相对应 不同的文件描述符也可能指向同一个文件 在L…...

华为OD机试-运维日志排序

文章目录题目描述输入描述输出描述&#xff1a;示例Java 代码实现题目描述 运维工程师采集到某产品线网运行一天产生的日志n条&#xff0c;现需根据日志时间先后顺序对日志进行排序&#xff0c;日志时间格式为H:M:S.N。 H表示小时(0~23) M表示分钟(0~59) S表示秒(0~59) N表…...

1Kotlin基础知识

1 变量 1.1 用法 Kotlin中的变量定义有2个关键字&#xff0c;val和var val用来定义不可变变量&#xff0c;第一次赋值后就不能再被修改了&#xff0c; var定义可变变量&#xff0c; 随便修改。一个好的编程习惯是&#xff0c; 能用val的就不要用var&#xff0c; 原因是安全&a…...

Redis Lua脚本

文章目录一.引言二.eval简介三.lua数据类型和redis数据类型之间转换四.脚本的原子性五.错误处理六.纯函数脚本七.选择内部脚本一.引言 eval和evalsha命令使用内置的lua解释器&#xff0c;可以对lua脚本进行求值。 二.eval简介 第一个参数是一段脚本程序第二个参数是参数的个…...

web自动化测试-执行 JavaScript 脚本

JavaScript 是一种脚本语言&#xff0c;有的场景需要使用 js 脚本注入辅助我们完成 Selenium 无法做到的事情。 当 webdriver 遇到无法完成的操作时&#xff0c;可以使用 JavaScript 来完成&#xff0c;webdriver 提供了 execute_script() 方法来调用 js 代码。 执行 js 有两种…...

libevent笔记——简单介绍

背景 libevent libevent – an event notification library 官方定义&#xff1a;libevent是一个事件通知的库。更详细的介绍参考官方的就够了&#xff0c;这里我摘抄一下&#xff0c;并做一些注释 The libevent API provides a mechanism to execute a callback function whe…...

C++学习笔记-多态

多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c; 具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会 产生出不同的状态 。 举个例子&#xff1a;比如 买票这个行为 &#xff0c;当 普通人 买票时&#xff0c;是全价买票&#xff1b…...

5632: 三角形

描述平面坐标系下&#xff0c;给定不共线的三个点组成一个三角形&#xff0c;问三角形最短的边长和最长的边长各为多少&#xff1f;输入输入包含3行&#xff0c;每行两个整数&#xff0c;表示一个点的坐标x和y。输出输出包括2个小数&#xff0c;分别为最短的边长和最长的边长。…...

Java基础--IO操作

一、IO原理及分类 一、IO原理 1、I/O是Input/Output的缩写&#xff0c;I/O技术是非常实用的技术&#xff0c;用于处理设备之间的数据传输&#xff0c;如读写文件&#xff0c;网络通信等。 2、java程序中对于数据的输入/输出操作一般都是以流的方式进行 3、java.io包下提供各…...

C++多线程

目录一、C线程库1. 认识thread类2. 线程函数的参数3. this_thread二、原子操作三、C互斥锁1. mutex2. lock_guard3. unique_lock四、C条件变量1. condition_variable2. 实现两个线程交替打印奇偶数一、C线程库 1. 认识thread类 在C11之前没有多线程的概念&#xff0c;涉及到的…...

【Arduino使用nRF24L01 】

【Arduino使用nRF24L01 】 1. 概述2. nRF24L01 收发器模块2.1工作原理2.2 NRF24L01模块变体2.3 nRF24L01 模块引脚排列3. 如何将 nRF24L01 连接到 Arduino3.1 原理接线图3.2 Arduino 和 nRF24L01 代码3.3 代码说明4. 故障排除5. 两个NRF24L01和Arduino进行双向无线通信5.1 nRF2…...

Appium自动化测试框架是一种较为优雅的使用方式

以操作小米商城下单为例流程是 启动小米商城app, 点击分类&#xff0c;点击小米手机&#xff0c; 点击小米10 至尊版&#xff0c;点击加入购物车&#xff0c;点击确定....原脚本Copyfrom time import sleep from appium import webdriver from selenium.common.exceptions impo…...

Linux c编程之应用交互协议分析与设计

在实际编程应用中,两个或多个功能服务(模块)之间 需要通过消息交互进行协作完成用户想要的逻辑功能,这里的消息交互指的是应用层的交互。最终数据传输(无论是TCP/IP还是其它)都是以二进制形式完成,但对于应用层协议来说有两种,一种是二进制协议,一种是文本协议。不管是…...

基于YOLOv5的细胞检测实战

数据及代码链接见文末 1.任务与数据集介绍 如下图所示,我们有一个医学细胞数据集,需要从数据集中检测出三种不同的细胞。标签中已经标注了细胞的类别和位置。 我们也可以看到,三种细胞有着不同的形态和颜色,同时数据集的标签也存在没有标注到的细胞 2.数据与标签配置方…...

【经典蓝牙】蓝牙AVRCP协议分析

协议简介 蓝牙AVRCP协议是蓝牙设备之间音视频的控制协议。定义了音频/视频的控制、浏览、查询、通知等一系列的命令集。常用来蓝牙耳机对手机的音乐进行控制&#xff0c;以及获取手机的音乐信息等场景。AVRCP协议有两个角色&#xff0c;分别是controller&#xff08;CT&#x…...

gin 框架初始教程

一 、gin 入门1. 安装gin &#xff1a;下载并安装 gin包&#xff1a;$ go get -u github.com/gin-gonic/gin2. 将 gin 引入到代码中&#xff1a;import "github.com/gin-gonic/gin"3.初始化项目go mod init gin4.完整代码package mainimport "github.com/gin-go…...