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

【并发编程】详解volatile

【并发编程】

    • 一、volatile是什么?
    • 二、volatile的三大核心特性
      • 1. 可见性
      • 2. 顺序性
      • 3. 不保证原子性
    • 三、volatile的底层实现原理
      • 1. 内存屏障的作用
      • 2. volatile变量的内存屏障插入规则
    • 四、volatile和synchronized的核心区别
    • 五、volatile的典型使用场景
      • 1. 状态标志位
      • 2. 双重检查锁的单例模式
      • 3. 轻量级的共享变量传递
    • 扩展:volatile和AtomicInteger

volatile是什么?它能解决什么问题?它的底层实现原理是什么?和synchronized有啥区别?哪些场景适合用volatile?

一、volatile是什么?

在Java并发编程里,多线程操作共享变量时,经常会出现“线程A改了变量,线程B却看不到”的情况。这时候,volatile就能派上用场——它是Java提供的轻量级同步关键字,专门用来保证共享变量的“可见性”,同时还能禁止“指令重排序”

简单说,当一个变量被volatile修饰后,它就有了两个核心特性:

  1. 线程对变量的修改会立刻同步到主内存,不会只停留在自己的工作内存里;
  2. 其他线程读取这个变量时,会直接从主内存加载最新值,而不是用自己工作内存里的旧数据。

举个例子:没有volatile修饰时,线程1把flag改成true,线程2可能一直读的是自己工作内存里的false,导致循环无法结束;加了volatile后,线程1改完flag会马上同步到主内存,线程2每次读flag都会从主内存拿最新值,能立刻感知到变化。

// 没有volatile,线程2可能永远看不到flag的变化
private boolean flag = false;// 有volatile,线程2能实时感知flag的修改
private volatile boolean flag = false;

二、volatile的三大核心特性

1. 可见性

这是volatile最核心的作用。要理解可见性,得先知道Java的“内存模型”(【并发编程】彻底搞懂Java内存模型 JMM:从底层原理到并发问题解决):

  • 每个线程都有自己的“工作内存”,操作变量时会先把主内存的变量拷贝到工作内存;
  • 线程修改变量后,会先更新工作内存里的值,再“不定期”同步回主内存;
  • 其他线程读变量时,也会“不定期”从主内存刷新工作内存里的副本。

这种“不定期”就导致了可见性问题。而volatile能强制线程做到两点:

  • 修改变量后,立刻将工作内存的新值刷回主内存
  • 读取变量前,先将工作内存的旧值清空,再从主内存重新加载

这样一来,所有线程操作的都是主内存的“最新值”,不会出现“一个改了、一个没看见”的情况。

2. 顺序性

编译器和CPU为了提高性能,会对代码执行顺序做“合理调整”——这就是指令重排序。平时单线程下没问题,但多线程下可能出大错。

比如单例模式的“双重检查锁”,没有volatile修饰instance时,可能会出现“对象还没初始化完,就被其他线程拿走用”的情况:

// 有问题的双重检查锁(缺少volatile)
public class Singleton {private static Singleton instance; // 没有volatilepublic static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton(); // 这里可能被重排序}}}return instance;}
}

new Singleton()其实分三步:

  1. 分配内存空间;
  2. 初始化对象;
  3. instance指向内存空间。

编译器可能会把步骤2和3重排序成“1→3→2”。这时候线程A执行到instance = new Singleton(),刚做完步骤3(instance不为null),还没做步骤2(对象没初始化);线程B过来判断instance != null,直接拿走没初始化的对象,一用就报错。

而volatile能禁止这种重排序,强制new操作按“1→2→3”的顺序执行,避免上述问题。

3. 不保证原子性

这是volatile最容易被误解的点——很多人以为它能替代synchronized,解决所有并发问题,但其实它管不了“多个线程同时修改变量”的原子性问题

比如两个线程同时执行count++,即使count被volatile修饰,最终结果也可能小于预期值。因为count++不是“一步操作”,而是分三步:

  1. 从主内存读取count的最新值;
  2. 在工作内存里把count加1;
  3. 把新值刷回主内存。

这三步中间可能被其他线程打断。比如线程A读了count=10,还没来得及加1,线程B就也读了count=10;之后A加1变成11刷回主内存,B加1也变成11刷回主内存——原本该是12的结果,最终成了11。

所以,要解决原子性问题,还得用synchronizedReentrantLock或者AtomicInteger这类原子类。

三、volatile的底层实现原理

volatile的特性是依赖CPU的“内存屏障”(Memory Barrier)指令实现的。Java虚拟机在处理volatile变量时,会在生成的字节码里插入特定的内存屏障,从而约束编译器和CPU的行为。

1. 内存屏障的作用

内存屏障有两个核心功能:

  • 禁止屏障两侧的指令重排序;
  • 强制将工作内存的缓存数据刷回主内存(写屏障),或强制从主内存重新加载数据(读屏障)。

2. volatile变量的内存屏障插入规则

Java虚拟机会给volatile变量的读写操作加上不同的内存屏障,具体规则如下:

  • 写操作后:插入“StoreStore屏障”和“StoreLoad屏障”。
    • StoreStore屏障:保证在当前volatile变量写操作之前,所有普通变量的写操作都已经刷回主内存;
    • StoreLoad屏障:保证当前volatile变量的写操作已经刷回主内存后,再执行后续的读操作。
  • 读操作前:插入“LoadLoad屏障”和“LoadStore屏障”。
    • LoadLoad屏障:保证在读取当前volatile变量之前,先清空工作内存的旧数据,从主内存加载最新值;
    • LoadStore屏障:保证当前volatile变量的读操作完成后,再执行后续普通变量的写操作。

正是这些内存屏障,让volatile实现了“可见性”和“禁止重排序”的特性。

四、volatile和synchronized的核心区别

很多人会把volatile和synchronized(【并发编程】深入理解Synchronized:从并发问题到锁升级的完整解析)搞混,其实它们的定位和能力完全不同,核心区别有4点:

对比维度volatilesynchronized
作用层级变量级(只修饰变量)代码块/方法级(修饰代码块或方法)
原子性不保证保证(同一时间只有一个线程执行)
可见性保证(通过内存屏障)保证(释放锁时刷回主内存)
有序性保证(禁止重排序)保证(单线程执行,天然有序)
性能轻量级,几乎无开销重量级,有锁竞争时会阻塞线程

简单地说:volatile是“轻量级同步手段”,只解决可见性和有序性,适合变量的“单次读/写”场景;synchronized是“重量级锁”,能解决原子性、可见性、有序性所有问题,但性能开销更大,适合复杂的同步逻辑。

五、volatile的典型使用场景

volatile不是“万能药”,但在某些场景下,它比synchronized更高效,是最佳选择。

1. 状态标志位

这是volatile最常用的场景——用一个volatile变量作为“线程间的信号开关”,控制线程的启动、停止或执行逻辑切换。

比如线程A负责循环执行任务,线程B通过修改volatile修饰的stopFlag,让线程A停止循环:

public class VolatileDemo {// 用volatile修饰状态标志位private volatile boolean stopFlag = false;// 线程A:循环执行任务,直到stopFlag为truepublic void startTask() {new Thread(() -> {while (!stopFlag) {// 执行具体任务System.out.println("线程A正在执行任务...");}System.out.println("线程A停止执行");}).start();}// 线程B:修改stopFlag,让线程A停止public void stopTask() {stopFlag = true;}
}

这里用volatile正好合适:stopFlag的操作是“单次写”(线程B)和“单次读”(线程A),没有原子性问题,用volatile保证可见性即可,性能比synchronized高。

2. 双重检查锁的单例模式

前面提到过,双重检查锁的单例模式里,instance必须用volatile修饰,否则会因为指令重排序出现“对象未初始化完成就被使用”的问题。正确的写法如下:

public class Singleton {// 关键:用volatile修饰instanceprivate static volatile Singleton instance;private Singleton() {} // 私有构造,防止外部newpublic static Singleton getInstance() {if (instance == null) { // 第一次检查(无锁,提高效率)synchronized (Singleton.class) { // 加锁if (instance == null) { // 第二次检查(防止多线程重复创建)instance = new Singleton(); // 禁止重排序}}}return instance;}
}

3. 轻量级的共享变量传递

当多个线程需要共享一个“单次赋值、多次读取”的变量时,用volatile修饰可以保证所有线程读到的都是最新值。

比如线程A加载配置文件后,把配置对象赋值给volatile修饰的config变量;其他线程读取config时,能立刻拿到最新的配置,不用加锁。

扩展:volatile和AtomicInteger

很多人会问:既然volatile不保证原子性,那AtomicInteger为什么能保证原子性?其实AtomicInteger的底层是“volatile+CAS(Compare And Swap)”的组合。

  • AtomicInteger的value变量被volatile修饰,保证可见性;
  • 它的incrementAndGet()等方法,通过CPU的CAS指令实现“原子性修改”——CAS会先比较value的当前值和预期值,如果一致才修改,不一致就重试,直到成功。

简单说:volatile负责“看见最新值”,CAS负责“修改时不被打断”,二者结合才实现了原子性。而volatile单独使用时,没有CAS的“比较-修改”逻辑,所以解决不了原子性问题。

http://www.dtcms.com/a/463045.html

相关文章:

  • 商务网站建设与维护实训报告网站建设的工作计划
  • 佛山电商网站制作那些网站是静态
  • 网络营销的发展趋势太原网站排名优化价格
  • 基于Qt框架开发的IP地址输入控件
  • Redis高可用与扩展性深度解析:主从复制、哨兵与集群
  • 深入理解手机快充技术:原理、协议与嵌入式实现
  • 小杰深度学习(seven)——卷积神经网络——池化
  • gSOAP: 一个C++构建Web服务和跨语言开发的利器
  • 网站简易后台计算机网站开发毕业设计论文开题报告
  • 广东网站建设公电子商务网站建设与管理课后题答案6
  • 个人网站不能有盈利性质wordpress 自定义分类 模板
  • 充值选建设银行打不开网站360免费wifi上不了网
  • 微信网站下载亚马逊雨林面积有多大
  • AI AgenticAI 教程:让AI成为学习与创作的智能伴侣
  • 跳舞游戏做的广告视频网站广告设计接单网站
  • 动画设计招聘信息太原seo管理
  • 再见用数字怎么表达?
  • DOM Comment
  • 举报非法网站要求做笔录淘宝权重查询入口
  • 自适应型网站建设网站设计学什么专业
  • 网站维护的基本概念营销网络是啥意思
  • 可以看禁止访问网站的浏览器做网站用是内网穿透好
  • 精选合肥网站建设网站建设所需要的材料
  • 集成mybatis
  • 做投诉网站赚钱吗平面图用什么软件做
  • 湛江网站建设方案报价wordpress 调用
  • 公司简介网站模板天津专业做网站
  • Unity中MonoBehavior类中的延迟函数Invoke详解(含案例)
  • app软件下载网站源码无锡企业网站
  • 公司注册网站建设延庆免费网站建设