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

news2024/12/23 7:06:38

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=0
        0: getstatic        #13; //Field race:I
        3: iconst_1
        4: iadd
        5: putstatic        #13; //Field race:I
        8: return
    LineNumberTable:
        line 14: 0
        line 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 虚拟机》

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

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

相关文章

这9道软件测试面试题,就能刷掉90%的软件测试员

转眼就要到“金三银四”了&#xff0c;没点真本事真技术&#xff0c;没点面试经验&#xff0c;不了解点职场套路&#xff0c;如何过五关斩六将&#xff1f;如何打败面试官&#xff1f;如何拿下那梦寐以求的offer&#xff1f; 如果你的跳槽意向已经很确定&#xff0c;那么请往下…

【python量化】大幅提升预测性能,将NSTransformer用于股价预测

写在前面NSTransformer模型来自NIPS 2022的一篇paper《Non-stationary Transformers: Exploring the Stationarity in Time Series Forecasting》。NSTransformer的目的主要是为了解决其他方法出现过平稳化处理的问题。其通过提出序列平稳化以及去平稳化注意力机制可以使得模型…

我眼中的柔宇科技

关注、星标公众号&#xff0c;直达精彩内容来源&#xff1a;技术让梦想更伟大作者&#xff1a;李肖遥很早就知道了柔宇科技&#xff0c;当时是因为知道创始人刘自鸿&#xff0c;23岁清华本硕毕业&#xff0c;26岁获斯坦福大学电子工程博士学位&#xff0c;历时不超过3年&#x…

你真的了解环形队列吗?(学习数据结构必须掌握的模型)

目录 0.前言 1. 什么是环形队列 2. 如何使用数组结构 / 链表结构 对环形队列封装 3. 代码手撕环形队列各个接口 3.1 代表封装一个环形队列 3.2 环形队列的初始化 3.3 环形队列的插入 3.4环形队列的删除 3.5环形队列的判空 3.6环形队列的判满 3.7环形队列的队头 3.8环…

NCNN量化详解2

1 NCNN量化算法简介 量化算法介绍的文章的话,下面这篇文章的大佬 @章小龙 介绍的比我好多啦。虽然介绍的是NVIDIA TensorRT的算法,但是NCNN是参考其算法做出来的,方法几乎一样 首先NVIDIA有一个PPT,很好的阐述了他们的方案,推荐大家也去看一下: https://link.zhihu.com…

我来了,RK第七届开发者大会

大家好&#xff0c;我是发哥的朋友windsnake「也是发哥文章里面经常出现的邓总」。今年终于是有时间参加了一次Rk的开发者大会&#xff0c;这篇文章记录下自己的所见所闻&#xff0c;算是对自己福州之行的一次总结。从深圳出发月亮还没睡醒的早上&#xff0c;坐5号线杀到深圳北…

【项目设计】高并发内存池(二)[高并发内存池整体框架设计|threadcache]

&#x1f387;C学习历程&#xff1a;入门 博客主页&#xff1a;一起去看日落吗持续分享博主的C学习历程博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 也许你现在做的事情&#xff0c;暂时看不到成果&#xff0c;但不要忘记&…

【无标题String、StringBuffer、StringBuilder区别】

一、背景。 这篇文章主要介绍了String、StringBuffer、StringBuilder的区别详细教程,本文通过图文并茂的形式给大家介绍的非常详细&#xff0c;对大家的学习或工作具有一定的参考借鉴价值&#xff0c;需要的朋友可以参考下。放假在家里休息&#xff0c;闲来无事&#xff0c;想…

华为OD机试题,用 Java 解【汽水瓶】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…

【华为OD机试模拟题】用 C++ 实现 - 寻找路径 or 数组二叉树(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 获得完美走位(2023.Q1) 文章目录 最近更新的博客使用说明寻找路径 or 数组二叉树题目输入输出描述示例一输入输出示例二输入输出Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过…

华为开源自研AI框架昇思MindSpore数据变换:Transforms

目录一、环境准备1.进入ModelArts官网2.使用CodeLab体验Notebook实例二、数据变换 TransformsCommon TransformsComposeVision TransformsRescaleNormalizeHWC2CWHText TransformsBasicTokenizerLookupLambda Transforms通常情况下&#xff0c;直接加载的原始数据并不能直接送入…

线程安全之synchronized和volatile

目录 1.线程不安全的原因 2.synchronized和volatile 2.1 synchronized 2.1.1 synchornized的特性 2.1.2 synchronized使用示例 2.2 volatile 我们先来看一段代码&#xff1a; 分析以上代码&#xff0c;t1和t2这两个线程的任务都是分别将count这个变量自增5000次&#xff…

redis(5)列表List

Redis列表 Redis单键多值&#xff1a;Redis 列表是简单的字符串列表&#xff0c;按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。 它的底层实际是个双向链表&#xff0c;对两端的操作性能很高&#xff0c;通过索引下标的操作中间的节点性能会较差。 常…

【Linux学习笔记】7.Linux vi/vim

前言 本章介绍Linux的vi/vim。 Linux vi/vim 所有的 Unix Like 系统都会内建 vi 文书编辑器&#xff0c;其他的文书编辑器则不一定会存在。 但是目前我们使用比较多的是 vim 编辑器。 vim 具有程序编辑的能力&#xff0c;可以主动的以字体颜色辨别语法的正确性&#xff0c…

【华为OD机试模拟题】用 C++ 实现 - 优秀学员统计(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 货币单位换算(2023.Q1) 【华为OD机试模拟题】用 C++ 实现 - 选座位(2023.Q1) 【华为OD机试模拟题】用 C++ 实现 - 停车场最大距离(2023.Q1) 【华为OD机试模拟题】用 C++ 实现 - 重组字符串(2023.Q1) 【华为OD机试模…

HashMap数据结构

HashMap概述 HashMap是基于哈希表的Map接口实现的&#xff0c;它存储的是内容是键值对<key,value>映射。此类不保证映 射的顺序&#xff0c;假定哈希函数将元素适当的分布在各桶之间&#xff0c;可为基本操作(get和put)提供稳定的性能。 HashMap在JDK1.8以前数据结构和存…

网络流与图(三)

经过两篇文章的篇幅&#xff0c;我们介绍了最小费用网络流模型以及解决的算法。今天我们介绍网络流模型的现实应用案例&#xff0c;并针对一些特殊的情景提出更高效的解决算法。传送门&#xff1a;网络流与图&#xff08;一&#xff09;网络流与图&#xff08;二&#xff09;1运…

多模态预训练模型综述

经典预训练模型还未完成后续补上预训练模型在NLP和CV上取得巨大成功&#xff0c;学术届借鉴预训练模型>下游任务finetune>prompt训练>人机指令alignment这套模式&#xff0c;利用多模态数据集训练一个大的多模态预训练模型&#xff08;跨模态信息表示&#xff09;来解…

【数据结构】栈的接口实现(附图解和源码)

栈的接口实现&#xff08;附图解和源码&#xff09; 文章目录栈的接口实现&#xff08;附图解和源码&#xff09;前言一、定义结构体二、接口实现&#xff08;附图解源码&#xff09;1.初始化栈2.销毁栈3.入栈4.判断栈是否为空5.出栈6.获取栈顶元素7.获取栈中元素个数三、源代码…

【华为OD机试模拟题】用 C++ 实现 - 字符匹配(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 获得完美走位(2023.Q1) 文章目录 最近更新的博客使用说明字符匹配题目输入输出示例一输入输出说明示例二输入输出说明Code使用说明 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。…