volatile 的作用

  • 保证变量的内存可见性
  • 禁止指令重排序

1.保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,其它线程每次使用前立即从主内存刷新。 但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)。


可见性

在理解 volatile 的内存可见性前,我们先来看看这个比较常见的多线程访问共享变量的例子。

/*** 变量的内存可见性例子*/
public class VolatileExample {/*** main 方法作为一个主线程*/public static void main(String[] args) {MyThread myThread = new MyThread();// 开启线程myThread.start();// 主线程执行for (; ; ) {if (myThread.isFlag()) {System.out.println("主线程访问到 flag 变量");}}}}/*** 子线程类*/
class MyThread extends Thread {private boolean flag = false;@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 修改变量值flag = true;System.out.println("flag = " + flag);}public boolean isFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}
}

执行上面的程序,你会发现,控制台永远都不会输出 “主线程访问到 flag 变量” 这句话。我们可以看到,子线程执行时已经将 flag 设置成 true,但主线程执行时没有读到 flag 的最新值,导致控制台没有输出上面的句子。

那么,我们思考一下为什么会出现这种情况呢?这里我们就要了解一下 Java 内存模型(简称 JMM)。

Java 内存模型

JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。

JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

JMM 的规定:
- 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  • 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
  • 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

JMM 的抽象示意图:

在这里插入图片描述

然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。

正因为 JMM 这样的机制,就出现了可见性问题。也就是我们上面那个例子出现的问题

那我们要如何解决可见性问题呢?接下来我们就聊聊内存可见性以及可见性问题的解决方案。

内存可见性

内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。

可见性问题的解决方案

我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。

这里有两种方案:加锁使用 volatile 关键字

下面我们使用这两个方案对上面的例子进行改造。

加锁

使用 synchronizer 进行加锁。

/*** main 方法作为一个主线程*/public static void main(String[] args) {MyThread myThread = new MyThread();// 开启线程myThread.start();// 主线程执行for (; ; ) {synchronized (myThread) {if (myThread.isFlag()) {System.out.println("主线程访问到 flag 变量");}}}}

这里大家应该有个疑问是,为什么加锁后就保证了变量的内存可见性了? 因为当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。

这里除了 synchronizer 外,其它锁也能保证变量的内存可见性。

使用 volatile 关键字

使用 volatile 关键字修饰共享变量。

/*** 子线程类*/
class MyThread extends Thread {private volatile boolean flag = false;@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 修改变量值flag = true;System.out.println("flag = " + flag);}public boolean isFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}
}

使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制 告知其他线程该变量副本已经失效,需要重新从主内存中读取。

volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。

接下来我们就聊聊一个比较底层的知识点:总线嗅探机制

总线嗅探机制

在现代计算机中,CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,在存取过程中,CPU 将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。

由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制

在这里插入图片描述

注意,缓存的一致性问题,不是多处理器导致,而是多缓存导致的。

嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。

可见性问题小结

上面的例子中,我们看到,使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。

下面我们来聊聊 volatile 的原子性问题。


原子性

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的

要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。

这里特别说一下,对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的


禁止指令重排序

什么是重排序?

有序性:即程序执行的顺序按照代码的先后顺序执行。

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:

在这里插入图片描述

虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?靠的是数据依赖性:

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

举例如下代码

double pi  = 3.14;    //A  
double r   = 1.0;     //B  
double area = pi * r * r; //C  

上面三个操作的数据依赖关系如下图所示

在这里插入图片描述

A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。下图是该程序的两种执行顺序:

在这里插入图片描述

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。编译器和处理器都遵从这一目标。
这里所说的数据依赖性仅针对
单个处理器中执行的指令序列和单个线程中执行的操作
,在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。这是就需要内存屏障来保证可见性了。

为了更好地理解重排序,请看下面的部分示例代码:

int a = 0;
flag = false;// 线程 A
a = 1;           // 1
flag = true;     // 2// 线程 B
if (flag) { // 3int i = a; // 4
}

单看上面的程序好像没有问题,最后 i 的值是 1。但是为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令做重排序(线程A中的a = 1; flag = true没有依赖关系)。假设线程 A 在执行时被重排序成先执行代码 2,再执行代码 1;而线程 B 在线程 A 执行完代码 2 后,读取了 flag 变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,那么 i 最后的值是 0,导致执行结果不正确。那么如何程序执行结果正确呢?这里仍然可以使用 volatile 关键字。

这个例子中, 使用 volatile 不仅保证了变量的内存可见性,还禁止了指令的重排序,即保证了 volatile 修饰的变量编译后的顺序与程序的执行顺序一样。那么使用 volatile 修饰 flag 变量后,在线程 A 中,保证了代码 1 的执行顺序一定在代码 2 之前。

那么,让我们继续往下探索, volatile 是如何禁止指令重排序的呢?这里我们将引出一个概念:内存屏障指令

内存屏障指令

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

内存屏障有两个作用:

1.阻止屏障两侧的指令重排序;
2.强制写入缓存中的最新数据更新写入主内存,让其他线程可见;让高速缓存中的数据失效,强制从新从主内存加载数据。

内存屏障分为两种:Store Barrier 和 Load Barrier 即 写屏障和读屏障。

  • 写屏障(Store Barrier)

    • 阻止屏障两侧的指令重排序。
    • 清空CPU的Store Buffer,将修改刷入主存,并触发缓存一致性协议(如MESI)广播Invalid信号,使其他核心的缓存行失效。

    读屏障(Load Barrier)

    • 阻止屏障两侧的指令重排序。
    • 强制CPU处理Invalidate Queue中的失效请求,若缓存行状态为Invalid,则从主存重新加载数据。

补充:

Store Buffer(存储缓冲区):位于 CPU 核心与 L1 缓存之间,优化写操作性能。

Invalidate Queue(失效队列):位于 L1 缓存与总线嗅探单元之间,加速对失效请求的响应。

java的内存屏障通常所谓的四种即StoreStore,StoreLoad,LoadLoad,LoadStore,实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

屏障类型核心作用防止的重排序
StoreStore保证普通写优先完成写-写重排序
StoreLoad确保写操作全局可见(全能屏障)写-读重排序
LoadLoad防止读操作重排序读-读重排序
LoadStore防止读-写重排序读-写重排序

下面我们来看看 volatile 读 / 写时是如何插入内存屏障的,见下图:

**在这里插入图片描述
**

从上图,我们可以知道 volatile 读 / 写插入内存屏障规则:

  • 在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。

    作用:禁止普通写与volatile写重排序;强制刷新写缓冲区,触发MESI广播失效信号。

  • 在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。

    作用:处理失效队列,强制加载最新值;禁止后续普通写与volatile读重排序。


缓存一致性协议&内存屏障

原理总结

  • 可见性通过内存屏障(强制刷缓存/失效缓存)+ MESI 协议(同步缓存状态)实现
  • 有序性通过内存屏障(禁止重排序)约束指令执行顺序

二者共同确保 volatile 变量的“可见性”与“有序性”,但不保证原子性(如 i++ 需配合锁或原子类)。

1. 缓存一致性协议(MESI)作用
  • 数据一致性:通过总线嗅探和状态机(Modified/Exclusive/Shared/Invalid)保证多核缓存数据一致。
  • 失效触发:
    • 核心A修改共享数据时,通过总线广播Invalid信号,使其他核心的缓存行失效(状态置为Invalid
    • 其他核心读取失效数据时,强制从主存重新加载最新值

2. 内存屏障作用
(1) 禁止重排序:确保操作有序性

指令重排序是编译器和CPU为优化性能对指令执行顺序的调整,单线程安全但多线程会导致逻辑错误。通过插入屏障指令,防止编译器和CPU对指令乱序执行。

(2) 强制同步内存:确保可见性与一致性

内存屏障通过强制刷新缓存状态,解决多核CPU因私有缓存(Store Buffer/Invalidate Queue)导致的数据不一致问题:

刷新写缓冲区(强制写操作全局可见)

  • 机制:
    • 写屏障(如StoreStore/StoreLoad)清空CPU的Store Buffer,将修改刷入主存,并触发缓存一致性协议(如MESI)广播Invalid信号,使其他核心的缓存行失效。
    • 示例volatile写后插入StoreLoad屏障,强制写缓冲区数据刷入主存,确保其他线程立即可见。

处理失效队列(强制读操作加载最新值)

  • 机制:
    • 读屏障(如LoadLoad/LoadStore)强制CPU处理Invalidate Queue中的失效请求,若缓存行状态为Invalid,则从主存重新加载数据。
    • 示例volatile读前插入LoadLoad屏障,确保读取前处理完所有失效请求,避免读到旧缓存。
(3) 与缓存一致性协议(MESI)的协同
  • MESI协议角色
    通过缓存行状态(Modified/Exclusive/Shared/Invalid)管理多核数据一致性,但​​无法主动保证顺序性​​。
  • 屏障与协议联动:
    • 写屏障触发MESI广播Invalid信号,使其他核心缓存失效;
    • 读屏障确保失效请求被处理,依赖MESI状态(如Invalid)重新加载数据。

happens-before

什么是happens-before?

一方面,程序员需要JMM提供一个强的内存模型来编写代码;另一方面,编译器和处理器希望JMM对它们的束缚越少越好,这样它们就可以最可能多的做优化来提高性能,希望的是一个弱的内存模型。

JMM考虑了这两种需求,并且找到了平衡点,对编译器和处理器来说,只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行。

而对于程序员,JMM提供了happens-before规则(JSR-133规范),满足了程序员的需求——简单易懂,并且提供了足够强的内存可见性保证。 换言之,程序员只要遵循happens-before规则,那他写的程序就能保证在JMM中具有强的内存可见性。

JMM使用happens-before的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程以内,也可以是不同的线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。

happens-before关系的定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见(保障可见性),而且第一个操作的执行顺序排在第二个操作之前(JMM对程序员做出的逻辑保障,并不是代码指令真正的执行保障)。
  2. 即使两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。

因此,

第一条是JMM对程序员做出的逻辑保障;

第二条是JMM对编译器、处理器进行重排序的约束原则:只有不改变程序的执行结果(不管是单线程还是多线程),编译器、处理器怎么优化都可以。

happens-before关系本质上和as-if-serial语义是一回事。

as-if-serial语义保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的,happens-before关系保证正确同步的多线程程序的执行结果不被重排序改变。

总之,如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的,不管它们在不在一个线程。

天然的happens-before关系

在Java中,有以下天然的happens-before关系:

  • 程序顺序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
  • 监视器锁规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果。
  • volatile 变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  • start() 规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
  • join() 规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。

从 happens-before 的 volatile 变量规则可知,如果线程 A 写入了 volatile 修饰的变量 V,接着线程 B 读取了变量 V,那么,线程 A 写入变量 V 及之前的写操作都对线程 B 可见。

这里特别说明一下,happens-before 规则不是描述实际操作的先后顺序,它是用来描述可见性的一种规则(前一个操作的结果对后续操作是可见的)。


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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Java获取本地PDF文件并解析数据

获取本地文件夹下的PDF文件要获取本地文件夹下的PDF文件,可以使用Java的File类和FilenameFilter接口。以下是一个示例代码片段:import java.io.File; import java.io.FilenameFilter;public class PDFFileFinder {public static void main(String[] args…

吴恩达机器学习补充:决策树和随机森林

数据集:通过网盘分享的文件:sonar-all-data.csv 链接: https://pan.baidu.com/s/1D3vbcnd6j424iAwssYzDeQ?pwd12gr 提取码: 12gr 学习来源:https://github.com/cabin-w/MLBeginnerHub 文末有完整代码,由于这里的代码和之前的按…

Shell脚本一键监控平台到期时间并钉钉告警推送指定人

1. 监控需求客户侧有很多平台需要定期授权,授权后管理后台才可正常登录,为避免授权到期,现撰写脚本自动化监控平台授权到期时间,在到期前15天钉钉或其他媒介提醒。2. 监控方案2.1 收集平台信息梳理需要监控的平台地址信息&#xf…

华为HCIE数通含金量所剩无几?考试难度加大?

最近网上很火的一个梗——法拉利老了还是法拉利,这句话套在华为HCIE数通身上同样适用,华为认证中的华为数通和云计算两大巨头充斥着大家的视野里面,也更加广为人知,但随着时代的发展,华为认证体系的调整,大…

#数据结构----2.1线性表

在数据结构的学习中,线性表是最基础、最核心的结构之一 —— 它是后续栈、队列、链表等复杂结构的 “基石”。今天从 “是什么”(定义)到 “怎么用”(基本操作),彻底搞懂线性表的核心逻辑。 一、先明确&…

2508C++,skia动画

gif动画原理 先了解一下gif动画的原理: gif动画由一系列静态图像(或叫帧)组成.这些图像按特定的顺序排列,每一帧都代表动画中的一个瞬间,帧图像是支持透明的. 每两帧之间有指定的时间间隔(一般小于60毫秒),gif播放器每渲染一帧静态图像后,即等待此时间间隔,依此逻辑不断循环渲染…

AI + 机器人:当大语言模型赋予机械 “思考能力”,未来工厂将迎来怎样变革?

一、引言1.1 未来工厂变革背景与趋势在科技飞速发展的当下,全球制造业正站在变革的十字路口。随着消费者需求日益多样化、市场竞争愈发激烈,传统工厂模式的弊端逐渐显现。生产效率低下、难以适应个性化定制需求、设备维护成本高昂且缺乏前瞻性等问题&…

pinia状态管理的作用和意义

1. 什么是状态管理 状态管理就是统一管理应用中的数据,让数据在多个组件之间共享和同步。 // 没有状态管理 - 数据分散在各个组件中 // 组件A const user ref({ name: 张三, age: 25 })// 组件B const user ref({ name: 张三, age: 25 }) // 重复定义// 组件C c…

十四、STM32-----低功耗

一、电源框图VDDA 供电区域,主要是 ADC 电源以及参考电压,STM32 的 ADC 模块配备独立的供电方 式,使用了 VDDA 引脚作为输入,使用 VSSA 引脚作为独立地连接,VREF 引脚为提供给 ADC 的 参考电压。电压调节器是 STM32 的…

一篇文章带你彻底搞懂 JVM 垃圾收集器

垃圾收集器是 JVM 内存管理的执行引擎,负责自动回收无用的对象内存。其设计核心是 权衡:主要是吞吐量和停顿时间之间的权衡。没有“最好”的收集器,只有“最适合”特定场景的收集器。一、核心基础:分代收集模型主流 HotSpot JVM 采…

服务器排故随笔:服务器无法ssh远程登录

文章目录服务器排故随笔:服务器无法远程登录问题现象解决过程第一步:确认故障描述是否准确第二步:确认网络是否有问题第三步:确认ssh服务是否有问题第四步:确认防火墙是否放行sshd服务第五步:试试万能的“重…

Deeplizard深度学习课程(六)—— 结合Tensorboard进行结果分析

前言 Tensorboard最初是tensorflow的可视化工具,被用于机器学习实验的可视化,后来也适配了pytorch。Tensorboard是一个前端web界面,,能够从文件里面读取数据并展示它(比如损失、准确率、网络图)。具体使用可…

C语言————实战项目“扫雷游戏”(完整代码)

无论是找工作面试,还是课设大作业、考研,都离不开实战项目的积累,如果你能把一个项目搞明白,并且给别人熟练的讲出来,即使你没有过项目经历,也可以说是非常加分的,下面来沉浸式体验一下这款扫雷…

数据结构之加餐篇 -顺序表和链表加餐

目录一、链表分割二、随机链表的复制总结一、链表分割 链表分割 题目描述的意思就如下图: 也就是把1,2挪到前面,6,3,5挪到后面,前者的相对顺序不发生改变 这里要想往后挪就要先遍历,遍历到6…

JSP与Servlet整合数据库开发:构建Java Web应用的全栈指南

JSP与Servlet整合数据库开发:构建Java Web应用的全栈指南 概述 在Java Web开发领域,JSP(JavaServer Pages)与Servlet是构建动态Web应用的核心技术组合。Servlet作为Java EE的基础组件,负责处理客户端请求、执行业务逻…

设计五种算法精确的身份证号匹配

问题定义与数据准备 我们有两个Excel文件: small.xlsx: 包含约5,000条记录。large.xlsx: 包含约140,000条记录。 目标:快速、高效地从large.xlsx中找出所有其“身份证号”字段存在于small.xlsx“身份证号”字段中的记录,并将这些匹配的记录保…

Spring 框架(IoC、AOP、Spring Boot) 的必会知识点汇总

目录:🧠 一、Spring 框架概述1. Spring 的核心功能2. Spring 模块化结构🧩 二、IoC(控制反转)核心知识点1. IoC 的核心思想2. Bean 的定义与管理3. IoC 容器的核心接口4. Spring Bean 的创建方式🧱 三、AOP…

简单工厂模式(Simple Factory Pattern)​​ 详解

✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。 🍎个人主页: Meteors.的博客 💞当前专栏: 设计模式 ✨特色专栏: 知识分享 &…

新电脑硬盘如何分区?3个必知技巧避免“空间浪费症”!

刚到手的新电脑,硬盘就像一间空荡荡的大仓库,文件扔进去没多久就乱成一锅粥?别急,本文会告诉你新电脑硬盘如何分区,这些方法不仅可以帮你给硬盘分区,还可以调整/合并分区大小等。所以,本文的分区…

【微知】git submodule的一些用法总结(不断更新)

文章目录综述要点细节如何新增一个submodule?如何手动.gitmodules修改首次增加一个submodule?git submodule init,init子命令依据.gitmodules.gitmodules如何命令修改某个成员以及同步?如果submodule需要修改分支怎么办&#xff1…