2023年Java核心技术第十二篇(篇篇万字精讲)

目录

22. AtomicInteger 底层实现原理是什么?如何在自己的项目代码中应用CAS操作?

22.1 典型回答

22.1.1 CAS详细解释:

22.1.1.1 预期值的选取:

22.1.2 CAS的弊端

22.1.2.1 ABA问题:

22.1.2.2 自旋次数限制:

22.1.2.3 只能保证一个共享变量的原子操作:

22.1.3 CAS操作失败的原因以及解决方案

22.1.3.1 CAS操作失败可能有以下几个原因:

22.1.3.1.1 竞争条件:

22.1.3.1.2共享变量被修改多次:

22.1.3.1.3自旋次数过少:

22.1.3.1.4并发度太高:

22.1.3.2 解决CAS操作一直失败的方法

22.1.3.2.1 调整自旋次数:

22.1.3.2.2 使用其他同步机制:

22.1.3.2.3 优化并发逻辑:

22.1.4 AtomicInteger的使用:

22.1.4.1  incrementAndGet()和decrementAndGet()解析

22.1.4.2  incrementAndGet()和decrementAndGet()底层伪代码进行解析

22.1.5 小结

22.1.5 ABA问题的解决方案

22.1.5.1 AtomicStampedReference补充解释


22. AtomicInteger 底层实现原理是什么?如何在自己的项目代码中应用CAS操作?

这一篇我们将走进各种同步结构,线程池,是基于什么原理来进行设计和实现。

22.1 典型回答

AtomicIntger 是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS技术。

22.1.1 CAS详细解释:

CAS,即Compare and Swap(比较并交换),是一种用于实现多线程同步的原子操作。

它在并发编程中广泛应用,用于解决多线程访问共享数据时可能出现的竞态条件问题。

CAS操作包含三个基本参数:内存地址V、旧的预期值A和新的目标值B。CAS操作通过比较内存地址V处的值与预期值A是否相等,如果相等,则将内存地址V处的值更新为目标值B;如果不相等,则说明其他线程已经修改了内存地址V处的值,当前线程的操作失败。

CAS操作可以简单理解为以下流程:

  1. 读取内存地址V处的值,记为当前值C。
  2. 判断C是否等于预期值A。如果相等,则继续执行;如果不相等,则说明其他线程已经修改了该值,当前线程的操作失败。
  3. 将目标值B写入内存地址V处。如果写入成功,则操作完成;如果写入失败,则返回第1步重新进行。

CAS操作是原子的,因为它在执行过程中,不会被其他线程中断或修改数据。

CAS的优势在于避免了使用锁机制带来的性能开销。相比于锁机制,CAS不需要阻塞线程,只需在操作失败时进行重试,从而减少了线程切换和上下文切换的开销。尤其在低竞争情况下,CAS能够提供较好的性能。

22.1.1.1 预期值的选取:

预期值(Expected Value)是在CAS操作中作为比较的参考值,用于判断内存地址中的值是否与预期值相等。预期值可以由程序员事先指定或者根据具体的上下文确定。

通常情况下,预期值是通过读取共享变量的当前值来获取的。在CAS操作执行之前,程序会先读取共享变量的当前值,并将其作为预期值传入CAS操作中。CAS操作会比较内存地址中的值与预期值是否相等,如果相等,则将新的目标值写入内存地址中;如果不相等,则说明其他线程已经修改了共享变量的值,当前线程的CAS操作失败。

需要注意的是,由于并发环境下共享变量的值可能会被其他线程修改,所以预期值只是一个参考值,并不能保证CAS操作一定成功。当多个线程同时进行CAS操作时,可能会出现竞争条件和重试的情况。

为了提高CAS操作的成功率,有时候可以根据上下文的需求,在选择预期值时采取一些策略,例如基于某种约束条件的判断、根据先前的操作结果等等。具体的预期值选择策略需要根据具体的业务场景和需求来确定。

22.1.2 CAS的弊端

 CAS存在一些限制和问题:

22.1.2.1 ABA问题:

如果内存地址V处的值在操作过程中经历了从A到B再到A的变化,CAS无法察觉到这种变化,可能导致数据不一致。

22.1.2.2 自旋次数限制:

为了避免出现无限循环,CAS通常会设置一个自旋次数的上限。如果达到了自旋次数的上限仍然没有成功,那么CAS操作将会失败,需要采取其他的同步机制。

22.1.2.3 只能保证一个共享变量的原子操作:

CAS只能对单个变量进行原子操作,对于多个变量之间的复合操作,CAS无法保证原子性。

CAS是一种基于比较和交换的原子操作,用于实现并发编程中的线程同步。它具有高效性和无锁特点,但也需要注意处理ABA问题和自旋次数限制等潜在问题。

22.1.3 CAS操作失败的原因以及解决方案

22.1.3.1 CAS操作失败可能有以下几个原因:

22.1.3.1.1 竞争条件:

多个线程同时执行CAS操作时,如果其中一个线程成功地更新了共享变量的值,那么其他线程的CAS操作就会失败。这是由于并发环境下存在竞争条件导致的。

22.1.3.1.2共享变量被修改多次:

CAS操作是基于共享变量当前的值进行比较和交换的。如果在CAS操作执行期间,共享变量被其他线程多次修改,那么CAS操作会失败,因为当前的值已经发生了变化。

22.1.3.1.3自旋次数过少:

如果设置的自旋次数过少,那么CAS操作可能没有足够的机会重试,从而直接失败。在高并发场景下,自旋次数的设置需要根据实际情况进行调优。

22.1.3.1.4并发度太高:

当并发度极高时,多个线程同时执行CAS操作,会增加CAS操作失败的概率。这是由于多个线程同时修改共享变量,导致彼此之间的竞争过于激烈。

22.1.3.2 解决CAS操作一直失败的方法

22.1.3.2.1 调整自旋次数:

增加自旋次数,使得CAS操作有更多的机会进行重试。但要注意自旋次数也不能设置过大,否则会占用过多的CPU资源。

22.1.3.2.2 使用其他同步机制:

如果CAS操作一直失败,可以考虑使用其他的同步机制,如互斥锁、信号量等。这些机制可以保证临界区的互斥访问,避免竞争条件的发生。

22.1.3.2.3 优化并发逻辑:

分析并发逻辑是否能够减少竞争条件的发生。例如,通过细粒度锁来减小锁的粒度,或者使用无锁数据结构等。这些优化措施可以减少竞争条件,从而降低CAS操作失败的概率。

22.1.4 AtomicInteger的使用:

当多个线程同时访问和更新一个普通的int类型变量时,可能会导致数据竞争(data race)的问题,从而引发并发访问的不一致性。为了解决这个问题,Java提供了AtomicInteger类,它是对int类型的一个封装,并提供了原子性的访问和更新操作。

CAS(compare-and-swap)是一种并发原语,用于实现多线程环境下的无锁同步。它包含三个操作数:内存地址(V)、期望值(A)和新值(B)。CAS的操作是原子的,当且仅当内存地址V上的值与期望值A相等时,才会将新值B写入到内存地址V上,并返回原来的值。

进行例子解析:

import java.util.concurrent.atomic.AtomicInteger;public class AtomicIntegerExample {private static AtomicInteger counter = new AtomicInteger(0);public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.incrementAndGet();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.decrementAndGet();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter.get()); // 输出结果应为0}
}

我们创建了一个AtomicInteger对象counter,初始值为0。然后创建两个线程thread1thread2,分别对counter进行自增和自减操作。在每次操作中,我们直接调用AtomicInteger提供的原子性方法,如incrementAndGet()decrementAndGet(),无需手动进行CAS操作。

由于AtomicInteger的操作是原子的,多个线程并发访问时不会出现数据竞争的问题,保证了计数器的正确性。最终,通过get()方法获取最终的结果。

22.1.4.1  incrementAndGet()和decrementAndGet()解析

incrementAndGet()decrementAndGet()是Java中AtomicInteger类提供的两个原子操作方法,用于对AtomicInteger对象进行自增和自减操作,并返回最新的值。

  • incrementAndGet(): 这个方法将当前值加1,并返回更新后的结果。它执行以下操作:

    1. 从内存中获取当前值。
    2. 将当前值增加1。
    3. 将新值写回内存。
    4. 返回最新的值。

    请注意,这个方法由于是原子操作,因此不会受到其他线程的干扰,即使多个线程同时调用incrementAndGet()也可以保证最终结果的正确性。

  • decrementAndGet(): 这个方法将当前值减1,并返回更新后的结果。它执行以下操作:

    1. 从内存中获取当前值。
    2. 将当前值减少1。
    3. 将新值写回内存。
    4. 返回最新的值。

    同样地,decrementAndGet()方法也是原子操作,保证了并发环境下的正确性。

这两个方法在并发编程中非常有用,特别适用于需要对共享计数器进行操作的场景。使用AtomicInteger类的incrementAndGet()decrementAndGet()方法,可以避免数据竞争和并发访问的问题,确保计数器的正确性和一致性。

如果只需要对计数器进行自增或自减操作,并不需要返回最新的值,可以使用incrementAndGet()decrementAndGet()方法的对应方法incrementAndGet()decrementAndGet()。这些方法只执行自增或自减操作,并不返回结果,因此更加高效。

例子:

import java.util.concurrent.atomic.AtomicInteger;public class AtomicIntegerExample {private static AtomicInteger counter = new AtomicInteger(0);public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.incrementAndGet();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.decrementAndGet();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter.get()); // 输出结果应为0}
}

我们创建了一个AtomicInteger对象counter,初始值为0。然后创建两个线程thread1thread2,分别对counter进行自增和自减操作。在每次操作中,我们直接调用AtomicInteger提供的原子性方法,如incrementAndGet()decrementAndGet()

由于AtomicInteger的操作是原子的,多个线程并发访问时不会出现数据竞争的问题,保证了计数器的正确性。最终,通过get()方法获取最终的结果。

以上代码的输出结果应该为0,因为thread1counter进行了1000次自增操作,thread2counter进行了1000次自减操作,两者抵消了。如果没有使用AtomicInteger类,而是直接操作普通的int变量,那么输出结果可能会不为0,因为数据竞争会导致错误的计算结果。

22.1.4.2  incrementAndGet()和decrementAndGet()底层伪代码进行解析

// 伪代码示例public int incrementAndGet() {while (true) {int current = getValue(); // 获取当前值int next = current + 1; // 计算下一个值if (compareAndSwap(current, next)) { // 比较并交换return next; // 返回更新后的值}// 如果比较并交换失败,则重新尝试}
}public int decrementAndGet() {while (true) {int current = getValue(); // 获取当前值int next = current - 1; // 计算下一个值if (compareAndSwap(current, next)) { // 比较并交换return next; // 返回更新后的值}// 如果比较并交换失败,则重新尝试}
}

getValue()方法用于获取当前存储的值。compareAndSwap(current, next)方法用于比较当前值和期望的旧值,并将新值写入内存中。

compareAndSwap(current, next)方法的实现:

private boolean compareAndSwap(int expect, int update) {if (currentValue == expect) { // 当前值和期望的旧值相等currentValue = update; // 将新值写入内存return true; // 操作成功} else {return false; // 操作失败}
}

CAS算法通过比较当前值和期望的旧值是否相等来判断是否发生了其他线程的修改。如果相等,说明没有发生竞争,将新值写入内存并返回操作成功;如果不相等,说明其他线程已经修改过值,需要重新尝试。

通过循环不断尝试,直到成功执行了比较并交换操作,incrementAndGet()decrementAndGet()方法保证了原子性的自增和自减操作。

22.1.5 小结

CAS其实就是Java的lock-free的基础,怎么进行理解。

CAS(Compare and Swap)是Java中实现无锁(lock-free)算法的基础。

理解CAS可以从以下几个方面:

  1. 原子性:CAS是一种原子操作,它能够在并发环境下实现无锁的原子更新操作。原子性意味着CAS操作要么完全执行成功,要么不执行,不会出现中间状态。这对于处理共享数据的并发访问非常重要。

  2. 比较和交换:CAS操作包括两个阶段:比较和交换。在比较阶段,CAS首先读取目标内存地址中的旧值,并和期望的旧值进行比较;如果相等,则进入交换阶段,CAS将新值写入到目标内存地址中;如果不相等,则说明该变量已经被其他线程修改过,CAS操作失败,需要重新尝试。

  3. 无锁算法:CAS是一种无锁算法,与传统的基于锁的同步方式不同。在使用锁的同步方式中,线程需要获取锁来访问共享资源,而CAS操作是无锁的,不需要获取锁。相比锁的方式,CAS操作避免了线程的阻塞和唤醒,减少了上下文切换的开销,提高了并发性能。

  4. ABA问题:尽管CAS是一种强大的原子操作,但它也存在ABA问题。ABA问题指的是如果一个值在操作期间被修改了两次,并恢复原值,那么CAS操作无法检测到这个过程。为了解决ABA问题,通常需要使用版本号或标记位等机制来追踪变量的变化。

CAS作为一种乐观锁技术,通过比较和交换来实现无锁的原子操作。它可以提高并发性能,减少锁带来的开销和竞争,但也需要注意处理ABA问题。CAS在Java中广泛应用于各种并发数据结构和算法的实现中,如Atomic类、ConcurrentHashMap、AQS等

22.1.5 ABA问题的解决方案

要解决ABA问题,可以使用版本号或标记位等机制来追踪变量的变化。这样可以在进行CAS操作时,不仅要比较值是否相等,还需要检查版本号或标记位是否发生变化。

例子:

有一个共享变量sharedValue,初始值为"A",并且有两个线程并发修改这个变量。

import java.util.concurrent.atomic.AtomicStampedReference;public class ABAExample {private static AtomicStampedReference<String> sharedValue = new AtomicStampedReference<>("A", 0);public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {int stamp = sharedValue.getStamp(); // 获取当前版本号sharedValue.compareAndSet("A", "B", stamp, stamp + 1); // 尝试将值从"A"更新为"B"sharedValue.compareAndSet("B", "A", stamp + 1, stamp + 2); // 尝试将值从"B"更新回"A"});Thread thread2 = new Thread(() -> {try {Thread.sleep(1000); // 等待一段时间,让thread1完成ABA操作} catch (InterruptedException e) {e.printStackTrace();}int stamp = sharedValue.getStamp(); // 获取当前版本号sharedValue.compareAndSet("A", "C", stamp, stamp + 1); // 尝试将值从"A"更新为"C"});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("Final value: " + sharedValue.getReference()); // 输出最终的共享变量值}
}

使用AtomicStampedReference来包装共享变量,并附上一个版本号。线程1先将共享变量从"A"更新为"B",然后再将其更新回"A",这个过程相当于一个ABA操作。而线程2在一段时间后将共享变量从"A"更新为"C"。

由于使用了版本号,CAS操作时不仅会比较值是否相等,还会比较版本号是否相等。在这个例子中,线程2尝试将值从"A"更新为"C"时,由于版本号已经发生变化,所以CAS操作会失败,不会误认为共享变量从"B"更新为"C",从而成功避免了ABA问题。

通过使用版本号或标记位等机制来追踪变量的变化,可以有效地解决ABA问题。

22.1.5.1 AtomicStampedReference补充解释

AtomicStampedReference是Java中的原子类,它提供了一种在进行CAS操作时同时维护一个版本号(stamp)的机制。

AtomicStampedReference可以用来解决ABA问题。ABA问题指的是如果一个值在操作期间被修改了两次,并恢复原值,那么CAS操作无法检测到这个过程,可能产生意外的结果。通过使用AtomicStampedReference,我们可以将变量的值和版本号一起进行比较和交换,从而避免误认为变量的值没有发生变化。

AtomicStampedReference的构造方法如下:

public AtomicStampedReference(V initialRef, int initialStamp)
  • initialRef:初始的引用值。
  • initialStamp:初始的版本号。

AtomicStampedReference提供了以下常用方法:

  • boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):如果当前引用和版本号与期望的引用和版本号相等,则将引用和版本号更新为新值。返回是否更新成功。

  • V getReference():获取当前的引用值。

  • int getStamp():获取当前的版本号。

  • void set(V newReference, int newStamp):设置引用和版本号为新值。

AtomicStampedReference还提供了其他一些方法来支持对引用和版本号的操作,例如weakCompareAndSet()attemptStamp()等。

通过使用AtomicStampedReference,我们可以在进行CAS操作时同时比较和交换变量的值和版本号,从而解决ABA问题。

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

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

相关文章

Java实现根据商品ID获取京东商品详情数据,1688商品详情接口,1688API接口封装方法

要通过京东的API获取商品详情数据&#xff0c;您可以使用京东开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例&#xff0c;展示如何通过京东开放平台API获取商品详情&#xff1a; 首先&#xff0c;确保您已注册成为京东开放平台的开发者&#xff0c;并创建一…

基于网卡序号双网卡数据共享(网卡转发)

基于网卡序号&#xff1a;ifr.ifr_ifindex; 实现网卡之间的数据转发 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <net/if.h> #include <sys/ioctl.h> #include <sys/types.h> #in…

经管博士科研基础【12】包络定理

当我们知道一个函数的最优解时&#xff0c;我们要求解这一个函数的值函数关于函数中某一个参数的导数&#xff0c;那么就可以使用包络定理。 1. 无约束条件下的包络定理 函数在其极值点处对一个参数&#xff08;参数不是自变量&#xff09;取偏导数的结果&#xff0c;等价于这…

【List】List集合有序测试案例:ArrayList,LinkedList,Vector(123)

List是有序、可重复的容器。 有序&#xff1a; List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问 元素&#xff0c;从而精确控制这些元素。 可重复&#xff1a; List允许加入重复的元素。更确切地讲&#xff0c;List通常允许满足 e1.equals(e2) 的元素…

Node.js crypto模块 加密算法

背景 微信小程序调用飞蛾热敏纸打印机&#xff0c;需要进行参数sig签名校验&#xff0c;使用的是sha1进行加密 // 通过crypto.createHash()函数&#xff0c;创建一个hash实例&#xff0c;但是需要调用md5&#xff0c;sha1&#xff0c;sha256&#xff0c;sha512算法来实现实例的…

小兔鲜商02

npm i vueuse/core -fvue插件使用&#xff1a; 许多公用的全局组件&#xff0c;&#xff0c;可以通过插件注册进去&#xff0c;就不用一个一个导入组件&#xff0c;&#xff0c; import XtxSkeleton from /components/library/xtx-skeletonexport default {install (app) {// …

C++并发编程:使用C++实现线程安全的栈

C并发编程&#xff1a;使用C实现线程安全的栈 引言 在多线程编程中&#xff0c;数据结构的线程安全性是至关重要的。本文将详细介绍如何使用C20标准库中的一些新特性来实现一个线程安全的栈。 什么是线程安全的栈&#xff1f; 简单来说&#xff0c;一个线程安全的栈是一个可…

linux操作系统中的动静态库(未完)

1. 静态库与动态库 静态库&#xff08;.a&#xff09;&#xff1a;程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库动态库&#xff08;.so&#xff09;&#xff1a;程序在运行的时候才去链接动态库的代码&#xff0c;多个程序共享使用库的…

为什么Java接口可以多继承,而类不可以?

个人主页&#xff1a;金鳞踏雨 个人简介&#xff1a;大家好&#xff0c;我是金鳞&#xff0c;一个初出茅庐的Java小白 目前状况&#xff1a;22届普通本科毕业生&#xff0c;几经波折了&#xff0c;现在任职于一家国内大型知名日化公司&#xff0c;从事Java开发工作 我的博客&am…

unity 跨屏显示

1.代码 /*Type:设置分辨率*/ using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using System.Runtime.InteropServices;public class ScreenManager : MonoBehaviour {[HideInInspector]//导入设置窗口函数 [DllImport("…

Redis之MoreKey问题及Scan命令解读

目录 MoreKey问题讨论 Scan命令 Sscan命令 Hscan命令 Zscan命令 MoreKey问题讨论 keys * 查看当前库所有key 对于海量数据执行key *会造成严重服务卡顿、影响业务。在实际环境中最好不要使用。生产制造过程中keys * / flushdb/flushall等危险命令以防止误删误用。 大量的…

kotlin实现猜数游戏

游戏规则 1.程序随机生成一个1到100的数字&#xff0c;作为MagicNumber 2.用户根据提示输入数据&#xff0c;只有三次机会输入数据 代码 代码很简单&#xff0c;使用了let内置函数 fun main() {//生成随机数可以使用java的方法//val magicNumber Random().nextInt(11)val ma…

如何确认linux的包管理器是yum还是apt,确认之后安装其他程序的时候就需要注意安装命令

打开终端 输入apt&#xff0c;下图中提示未找到命令&#xff0c;则基本上包管理工具就是用yum的 输入yum&#xff0c;我们看到有打印信息&#xff0c;则说明包管理工具是yum的&#xff0c;离线安装命令使用rpm

斥资4亿,收购这家WLAN厂商,结果……

晚上好&#xff0c;我的网工朋友 不少朋友可能有隐形&#xff0c;2019年的时候&#xff0c;Juniper花费4.05亿美元&#xff0c;收购WiFi初创公司Mist Systems。 Mist Systems是一家买无线产品起家的公司&#xff0c;由前思科高管创建的。主打的产品是“AI-Driven WLAN”&…

大数据平台测试-优秀员工申请模板

在企业里,优秀员工是需要申请的,下面分享一些模板: 1、申请个人优秀奖-测试质量奖 申报优秀员工-测试质量奖参考模板: 推动XXX质量共建的优秀实践者,通过验证的产研流程把控,过程问题的据理力争, 推动整体质量的大幅提升。在其强有力的执行和严格的把关下,XXX的质量管…

Kotlin 协程 - 多路复用 select()

一、概念 又叫选择表达式&#xff0c;是一个挂起函数&#xff0c;可以同时等待多个挂起结果&#xff0c;只取用最快恢复的那个值&#xff08;即多种方式获取数据&#xff0c;哪个更快返回结果就用哪个&#xff09;。 同时到达 select() 会优先选择先写子表达式&#xff0c;想随…

EI、Scopus双检索| 2023年第四届自动化、机械与设计工程国际会议

会议简介 Brief Introduction 2023年第四届自动化、机械与设计工程国际会议&#xff08;SAMDE 2023&#xff09; 会议时间&#xff1a;2023年12月8 -10日 召开地点&#xff1a;中国南京 大会官网&#xff1a;www.samde.org 机械设计制造及其自动化学科在国民经济中处于极其重要…

Java设计模式:一、六大设计原则-05:接口隔离原则

文章目录 一、定义&#xff1a;接口隔离原则二、模拟场景&#xff1a;接口隔离原则三、违背方案&#xff1a;接口隔离原则3.1 工程结构3.2 英雄技能调用3.2.1 英雄技能接口3.2.2 英雄&#xff1a;后裔3.2.3 英雄&#xff1a;廉颇 3.3 单元测试 四、改善代码&#xff1a;接口隔离…

QT6为工程添加资源文件,并在ui界面引用

以添加图片资源为例 右键工程名字&#xff08;不是最上面的名字&#xff09;&#xff0c;点击添加现有文件 这种方式虽然添加到了工程中&#xff0c;但不能在UI设计界面完成引用。主要原因可能是未把文件放入到项目资源文件中&#xff0c;以下面一种方式可以看出区别。 点击添…

常用的Spring Boot 注解及示例代码

简介&#xff1a;Spring Boot 是一个用于快速构建基于 Spring 框架的应用程序的工具&#xff0c;通过提供一系列的注解&#xff0c;它使得开发者可以更加轻松地配置、管理和控制应用程序的各种行为。以下是一些常用的 Spring Boot 注解&#xff0c;以及它们的功能和示例代码&am…