volatile详解、原理

文章目录

  • 一、Volatile的定义和作用
    • 1.1 Volatile简介
    • 1.2 Volatile作用
  • 二、并发编程中的三个问题:可见性、原子性、有序性
  • 二、Java内存模型(JMM)
  • 三、volatile变量的特性
    • 3.1 线程可见性
    • 3.2 禁止重排序
      • 禁止重排序原理
      • 禁止重排序举例
    • 3.3 volatile特性
  • 四、volatile原理
  • 五、适用场景、不适用场景
    • 5.1 volatile适用场景
    • 5.2 volatile不适用的场景
      • 5.2.1 volatile不适用 复合操作。
      • 5.2.2 如何解决
  • 六、Volatile与Synchronized比较

一、Volatile的定义和作用

1.1 Volatile简介

Java允许线程访问共享变量。为了确保共享变量能被一致、可靠地更新,线程必须确保 它是排他性地使用此共享变量,通常都是获得对这些共享变量强制排他性的同步锁。Java编程语言提供了另一种机制,volatile域变量,对于某些场景的使用 这会更加的方便。可以把变量声明为volatile,以让Java内存模型来保证所有线程都能看到这个变量的同一个值。

volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或同步方法) 和 volatile变量,相比于synchronized(synchronized通常被称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

注:Volatile只能修饰变量。

1.2 Volatile作用

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证线程间的可见性:当一个线程对共享变量进行了修改,另外的线程可以立即看到修改后的最新值。CPU都是有行缓存的,volatile能让行缓存无效,因此能读到内存中最新的值。

  • 禁止进行指令重排序:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

二、并发编程中的三个问题:可见性、原子性、有序性

还记得并发编程中的可见性、原子性、有序性吗?如果忘记可以到这里重温复习【并发编程】1 synchronized底层实现原理、Java内存模型JMM;可重入、不可中断、monitor、CAS、乐观锁和悲观锁;对象内存结构、Mark Word、synchronized锁升级

  • 可见性:当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。 ——> 解决:当多个线程访问同一个变量时,一个线程修改了该变量的值,其他线程能够立即看到修改后的最新值
  • 原子性:当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。 ——> 解决:在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行
  • 有序性:程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。 ——> 解决:程序执行的顺序按照代码的先后顺序执行

二、Java内存模型(JMM)

和Java内存结构不同,Java内存模型是一套规范、是标准化的,屏蔽掉了底层不同计算机的区别(Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则)。

JMM描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。细节如下。

  • 主内存:主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
  • 工作内存:每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

在这里插入图片描述

对于普通的共享变量x,线程1将其修改为某个值 发生在线程1的工作内存中,此时还未同步到主内存中去;而线程2已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程中的不可见问题,较粗暴的方式自然就是加锁,但此处使用synchronized或Lock这些方式 太重量级了,比较合理的方式是 volatile。

注意:JMM是抽象的内存模型,所谓的主内存、工作内存都是抽象概念,不一定就真实地对应cpu缓存、物理内存。

具体细节见【并发编程】1 synchronized底层实现原理、Java内存模型JMM;可重入、不可中断、monitor、CAS、乐观锁和悲观锁;对象内存结构、Mark Word、synchronized锁升级
在这里插入图片描述

三、volatile变量的特性

3.1 线程可见性

用 volatile 修饰共享变量,当一个线程对共享变量进行了修改,另外的线程可以立即看到修改后的最新值。CPU都是有行缓存的,volatile能让行缓存无效,因此能读到内存中最新的值。

  • 当某个线程写volatile变量、将修改的值同步回主内存时,JMM会把该线程工作内存中的变量强制刷新到主内存中去
  • JMM会把其他工作内存的值全部设置为失效,线程会重新读取共享内存的值

详解

当一个线程把主内存中的共享变量读取到自己的本地内存中,然后做了更新。在还没有把共享变量刷新的主内存的时候,另外一个线程是看不到的。如何把修改后的值刷新到主内存中?

现代的处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,较少对内存总线的占用。但是什么时候写入到内存是不知道的。

在这里插入图片描述

所以就引入了volatile,volatile是如何保证可见性的呢?

  • 将当前处理器缓存行的数据写回到系统内存。
  • 这个写回内存的操作会使其他cpu里缓存了该内存地址的数据无效。
  • 如果声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的还是旧的,再执行操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

3.2 禁止重排序

用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。

首先看下不加volatile的情况

1)修改pom.xml文件,添加依赖

<dependency><groupId>org.openjdk.jcstress</groupId><artifactId>jcstress-core</artifactId><version>0.14</version>
</dependency>

2)编写代码

//为什么使用此工具,直接运行代码不行吗?  只有使用大量的线程去执行,才能测试出指令重排序的效果
@JCStressTest     //用并发压缩工具jcstress 测试类方法
@Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ok")    //@Outcome 对输出结果进行处理, "0, 0", "1, 1", "0, 1"  打印ok
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestOrdering {int x;int y;@Actorpublic void actor1() {   //写x = 1;y = 1;}@Actorpublic void actor2(II_Result r) {   //读r.r1 = y;r.r2 = x;}
}

mvn clean install,java -jar xxx\target\jcstress.jar

3)查看项目对应路径results目录下的html文件,用浏览器打开,观察运行结果

在这里插入图片描述

在这里插入图片描述

根据运行结果,出现了”1,0“这种情况,说明actor1()方法先给y赋值、再给x赋值,确实发生指令重排序(指令重排序是为了实现指令流水线,属于计算机组成原理的内容)

如何解决:在变量上添加volatile,禁止指令重排序,则可以解决问题(volatile原理就是加了一些屏障,使屏障后的代码一定不会比屏障前的代码先执行,从而实现有序性)

  • 给 y 加上 volatile,不会出现”1,0“,解决了指令重排序的问题。
//省略部分代码
public class TestOrdering {int x;volatile int y;//其他不变,省略
}

在这里插入图片描述

volatile的底层原理是,给共享变量加上不同的屏障,保证指令不会发生重排序

  • 把volatile加在x上面,无法禁止指令重排序
//省略部分代码
public class TestOrdering {int x;volatile int y;//其他不变,省略
}

在这里插入图片描述

写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下,但无法阻止下方的指令往上走,y=1可以往上走、r.r1=y可以往下走。

  • 可以给x、y都加上volatile,也能解决问题,但性能不高,不推荐

在这里插入图片描述

在这里插入图片描述

禁止重排序原理

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为如下三种:

在这里插入图片描述

1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。

当变量声明为volatile时,Java编译器在生成指令序列时,会插入内存屏障指令,通过内存屏障指令来禁止重排序。

JMM内存屏障插入策略如下:

  • 在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障。
  • 在每个volatile读操作后面插入一个LoadLoad,LoadStore屏障。

Volatile写插入内存屏障后生成指令序列示意图:

在这里插入图片描述

Volatile读插入内存屏障后生成指令序列示意图:

在这里插入图片描述

通过上面这些我们可以得出如下结论:编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。

禁止重排序举例

从一个最经典的例子来分析重排序问题,以单例模式的双重检查锁实现(线程安全)为例

public class Singleton {public volatile static Singleton uniqueInstance;/*** 构造函数私有,禁止外部实例化*/private Singleton() {}public static Singleton getUniqueInstance() {if (uniqueInstance == null) {synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();}}}return uniqueInstance;}
}

为什么使用volatile关键字修饰uniqueInstance实例变量?

因为uniqueInstance = new Singleton()这段代码执行时分为三步:

  1. 为uniqueInstance分配内存空间
  2. 初始化uniqueInstance
  3. 将uniqueInstance指向分配的内存地址

正常的执行顺序当然是 1–>2–>3,但由于JVM具有指令重排的特性,执行顺序可能变成 1–>3–>2。单线程环境时,指令重排没有什么问题;多线程环境下,会导致有些线程可能会获取到还没初始化的实例。如线程A只执行了1和3,此时线程B来调用getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。

解决方法就是 加一个 volatile 关键字修饰 uniqueInstancevolatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。

3.3 volatile特性

  • volatile仅能使用在变量级别
  • volatile仅能实现变量的修改可见性,不能保证原子性,volatile + cas 就实现了原子性,如atomic包下面的类。
  • volatile不会造成线程的阻塞
  • volatile标记的变量不会被编译器优化

volatile可以保证可见性、有序性,无法保证原子性,所以是线程不安全的(它对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性,因为本质上i++是读、写两次操作)。要保证多线程的原子性, 可以通过AtomicInteger或者Synchronized来实现,本质上就是CAS操作(见下文5.2)

【synchronized可以保证可见性、原子性、有序性,因为锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性】

java.util.concurrent.*里面的高级线程安全数据结构像ConcurrentHashMap以及java.util.concurrent.atomic.*等的实现都用到了volatile。可以多看看这些类的实现,以加深对volatile的理解和运用。

四、volatile原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用"内存屏障"来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

计算机科学里面,为了解决复杂性,都会分层。正如一个名人所说:“计算机的任何问题都可以通过增加一个虚拟层来解决”(“All problems in computer science can be solved by another level of indirection”)。volatile虚拟机层引入的,解决语言层面的问题,那么它的实现,必然是靠下一层的支持,也就是需要汇编或者说处理器指令的支持来实现,volatile是靠内存屏障和**MESI**(缓存一致性协议)来达成的它的作用的。

内存屏障(Memory Barriers)是处理器提供的一组内存操作指令,它的作用是限制内存操作的顺序,也就是说内存屏障像一个栅栏一样,它前面的指令要在它后面的指令之前完成;还能强制把缓存写入到主存;再有的就是触发缓存一致性,就是当有写变量时,会把其他CPU核心的缓存变为无效。

五、适用场景、不适用场景

5.1 volatile适用场景

对变量可见性有要求、对读取顺序没要求的情况下。如 多线程情况下的标志位

  • 读操作,多于写操作
  • 写操作,不依赖于变量的当前值,即纯赋值操作
  • 只需要读取的值,不需要等待某一特定的值

5.2 volatile不适用的场景

5.2.1 volatile不适用 复合操作。

例如 number++ 不是一个原子性操作,由读取、加、赋值3步组成,下列程序可能返回多种结果,2677、4202、4722、4910、5000

public class Test02Atomicity {private volatile static int number = 0;public static void main(String[] args) throws InterruptedException {Runnable increment = () -> {for (int i = 0; i < 1000; i++) {number++;}};List<Thread> list = new ArrayList<>();for (int i = 0; i < 5; i++) {Thread t = new Thread(increment);t.start();list.add(t);}for (Thread thread : list) {//让主线程等待自己创建的线程执行完毕,5个线程执行完 再取number的值thread.join();}System.out.println("number=" + number);  //值有多种可能,2677、4202、4722、4910、5000}
}

5.2.2 如何解决

1)采用synchronized

public class Test02Atomicity {private static int number = 0;private static Object obj = new Object();public static void main(String[] args) throws InterruptedException {Runnable increment = () -> {for (int i = 0; i < 1000; i++) {synchronized (obj) {  number++; //synchronized保证 number++ 为原子操作。线程获取不到锁 就会等待}}};List<Thread> list = new ArrayList<>();for (int i = 0; i < 5; i++) {Thread t = new Thread(increment);t.start();list.add(t);}for (Thread thread : list) {//让主线程等待自己创建的线程执行完毕thread.join();}System.out.println("number=" + number);  //5000}
}

2)采用Lock

public class Test02Atomicity {private static int number = 0;private static Lock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Runnable increment = () -> {for (int i = 0; i < 1000; i++) {try {lock.lock();number++;} finally {lock.unlock();}}};List<Thread> list = new ArrayList<>();//使用5个线程来进行for (int i = 0; i < 5; i++) {Thread t = new Thread(increment);t.start();list.add(t);}for (Thread thread : list) {//让主线程等待自己创建的线程执行完毕,5个线程执行完 再取number的值thread.join();}System.out.println("number=" + number);  //5000}
}

3)采用java并发包中的原子操作类,原子操作类是通过CAS的方式来保证其原子性的

public class Test02Atomicity {private static AtomicInteger atomicInteger = new AtomicInteger();public static void main(String[] args) throws InterruptedException {Runnable increment = () -> {for (int i = 0; i < 1000; i++) {atomicInteger.incrementAndGet();   //AtomicInteger的incrementAndGet()可保证变量赋值的原子性}};List<Thread> list = new ArrayList<>();for (int i = 0; i < 5; i++) {Thread t = new Thread(increment);t.start();list.add(t);}for (Thread thread : list) {//让主线程等待自己创建的线程执行完毕thread.join();}System.out.println("atomicInteger=" + atomicInteger.get());  //5000}
}

六、Volatile与Synchronized比较

  • Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,因此Volatile性能更好。
  • Volatile只能修饰变量(类的成员变量、类的静态成员变量),synchronized可以修饰方法,静态方法,代码块。
  • Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
  • 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  • volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

参考 https://zhuanlan.zhihu.com/p/633426082、https://blog.csdn.net/u012723673/article/details/80682208、https://blog.csdn.net/xinghui_liu/article/details/124379221

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

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

相关文章

vue3 第二十八节 (vue3 事件循环之JS事件循环)

1、什么是事件循环 事件循环就是消息队列&#xff0c;是浏览器渲染主线程的工作方式&#xff1b; 过去将消息队列&#xff0c;简单的分为宏任务 和微任务 两种队列&#xff0c;而对于现在复杂多变的浏览器环境&#xff0c;显然这种处理方式已经不能满足使用&#xff0c;取而代…

AI试衣IDM-VTON,Windows11本地安装配置记录!

昨天我们已经介绍过IDM-VTON这个开源项目了。 通过这个软件可以轻松实现一键换衣服。 昨天&#xff0c;简单演示了一下在线使用。 今天&#xff0c;来演示如何安装到本地电脑上&#xff01; 本地配置会有一定的专业性&#xff0c;懂的人可以参考下。 不懂得直接拉到最后&am…

SSRF(服务器端请求伪造)的学习以及相关例题(上)

目录 一、SSRF的介绍 二、漏洞产生的原因 三、利用SSRF可以实现的效果&#xff08;攻击方式&#xff09; 四、SSRF的利用 五、SSRF中的函数 file_get_content() 、fsockopen() 、curl_exec() 1.file_get_content()&#xff1a; 2.fsockopen(): 3.curl_exec()&#xff1…

AI大模型客服机器人VS传统客服机器人

摘要&#xff1a;AI大模型客服机器人相较于传统客服机器人&#xff0c;具有更准确的语义理解、丰富的知识库、更自然的交互体验、个性化服务、自主学习和数据分析能力。传统客服机器人受限于规则和固定关键词&#xff0c;难以处理复杂查询和提供个性化服务。 1. 更准确的语义理…

正则表达式(python实例)

正则表达式&#xff08;python实例&#xff09; 一、简介 正则表达式&#xff08;Regular Expressions&#xff0c;简称 Regex&#xff09;是一种用来描述文本模式的工具&#xff0c;它通过一系列字符和特殊字符组合成的表达式&#xff0c;用于在文本中进行模式匹配和搜索。这…

【索引】索引基础

索引 在关系型数据库中&#xff0c;索引是一种用来帮助快速检索目标数据的存储结构。 索引的创建 MySQL可以通过CREATE、ALTER、DDL三种方式创建一个索引。 1、使用CREATE语句创建 CREATE INDEX indexName ON tableName (columnName(length) [ASC|DESC]);2、使用ALTER语句…

基于GD32的简易数字示波器(5)- 软件_控制LED

这期记录的是项目实战&#xff0c;做一个简易的数字示波器。 教程来源于嘉立创&#xff0c;帖子主要做学习记录&#xff0c;方便以后查看。 本期主要介绍GPIO口的输入输出模式&#xff0c;使用其中的输出模式驱动LED。详细教程可观看下方链接。 2.2 LED控制实验 语雀 1、LE…

SaaS(软件即服务)详解

SaaS&#xff08;软件即服务&#xff09;详解 引言 SaaS&#xff0c;即软件即服务&#xff0c;是一种通过互联网提供软件和服务的模式。与传统的软件购买和安装方式不同&#xff0c;SaaS 允许用户通过订阅的方式使用软件&#xff0c;通常是基于使用量或订阅期限进行计费。Saa…

传神论文中心|第8期人工智能领域论文推荐

在人工智能领域的快速发展中&#xff0c;我们不断看到令人振奋的技术进步和创新。近期&#xff0c;开放传神&#xff08;OpenCSG&#xff09;社区发现了一些值得关注的成就。多令牌预测方法的出现以及各类全新的多模态模型的发展令人耳目一新。传神社区本周也为对AI和大模型感兴…

IP SSL怎么签发使用

IP证书的签发首先是需要有一个可供绑定的IP地址&#xff0c;作为常用数字证书之一&#xff0c;IP证书也因为其广泛的应用范围而深得用户的青睐和喜欢。 部署IP证书后&#xff0c;可以实现该IP地址的https访问&#xff0c;过程和域名证书相差不多。 IP证书和域名证书的区别 很…

浅谈java,python,c++的差异

Java&#xff0c;Python和C是三种常见的编程语言&#xff0c;它们在很多方面有着不同的特点。以下是它们的一些主要异同点&#xff1a; 宏观应用 语法和风格&#xff1a; Java&#xff1a;Java是一种静态类型语言&#xff0c;语法相对严谨&#xff0c;需要显式声明变量的类型。…

python设计模式---工厂模式

定义了一个抽象类Animal&#xff0c;并且让具体的动物类&#xff08;Dog、Cat、Duck&#xff09;继承自它&#xff0c;并实现了speak方法。然后创建了AnimalFactory工厂类&#xff0c;根据传入的参数来决定创建哪种动物的实例。 from abc import abstractmethod, ABCclass Anim…

linux进阶高级配置,你需要知道的有哪些(9)-脚本应用(四)

1、正则表达式定义 是使用单个字符串来描述、匹配一系列符合某个句法规则的字符串 2、正则表达式组成 &#xff08;1&#xff09;普通字符 大小写字母、数字、标点符号及一些其他符号 &#xff08;2&#xff09;元字符 在正则表达式中具有特殊意义的专用字符 3、正…

05、 java 的三种注释及 javadoc 命令解析文档注释(即:java 特有注释方式)的过程

java的三种注释 1、单行注释&#xff1a;其一、代码展示&#xff1a;其二、特点&#xff1a; 2、多行注释&#xff1a;其一、代码展示&#xff1a;其二、特点&#xff1a; 3、文档注释(java特有)&#xff1a;其一、代码展示&#xff1a;其二、注释文档的使用&#xff1a;其三、…

掌握Vim:Linux系统维护的瑞士军刀 - 常用命令深度解析

在Linux的世界里&#xff0c;Vim编辑器犹如一位沉默的剑客&#xff0c;它的命令就是那锋利的剑刃&#xff0c;能够在代码的海洋中劈波斩浪。对于每一位Linux系统用户来说&#xff0c;掌握Vim的常用命令&#xff0c;就如同获得了维护系统的瑞士军刀。今天&#xff0c;让我们一起…

Java中,throw和throws的区别是什么?

在Java中&#xff0c;throw和throws都与异常处理相关&#xff0c;但它们的作用和使用方式有所不同。 throw 抛出异常&#xff1a;throw是一个语句&#xff0c;用于在程序的特定位置手动抛出一个异常对象。局部范围内&#xff1a;throw通常用在方法体或任何代码块的内部&#…

Rumor Remove Order Strategy on Social Networks

ABSTRACT 谣言被定义为广泛传播且没有可靠来源支持的言论。现代社会&#xff0c;谣言在社交网络上广泛传播。谣言的传播给社会带来了巨大的挑战。 “假新闻”故事可能会激怒您的情绪并改变您的情绪。有些谣言甚至会造成社会恐慌和经济损失。因此&#xff0c;谣言的影响可能是深…

Flutter 中的 Autocomplete 小部件:全面指南

Flutter 中的 Autocomplete 小部件&#xff1a;全面指南 在 Flutter 中&#xff0c;Autocomplete 是一个文本输入辅助小部件&#xff0c;它可以根据用户输入的内容提供自动完成建议。这在创建搜索栏、填写表单或其他需要快速输入的场景中非常有用。虽然 Flutter 的基础库中没有…

深入解析C#中的async和await关键字

文章目录 一、异步编程的基本概念及其在C#中的实现二、async关键字的定义及其用法三、await关键字的定义及其用法示例代码&#xff1a;使用async和await编写一个简单的异步程序 四、async和await的优点注意事项 五、C#下async和await中常见问题汇总1. 异步方法中的await调用2. …

STM32(GPIO)

GPIO简介 GPIO&#xff08;General Purpose Input Output&#xff09;通用输入输出口 引脚电平&#xff1a;0V~3.3V&#xff0c;部分引脚可容忍5V 输出模式下可控制端口输出高低电平&#xff0c;用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等 输入模式下可读取端口的高低电…