走进volatile的世界,探索它与可见性,有序性,原子性之间的爱恨情仇!
写在开头
在之前的几篇博文中,我们都提到了 volatile 关键字,这个单词中文释义为:不稳定的,易挥发的,在Java中代表变量修饰符,用来修饰会被不同线程访问和修改的变量,对于方法,代码块,方法参数,局部变量以及实例常量,类常量多不能进行修饰。
自JDK1.5之后,官网对volatile进行了语义增强,这让它在Java多线程领域越发重要!因此,我们今天就抽一晚上时间,来学一学这个关键字,首先,我们从标题入手,思考这样的一个问题:
volatile是如何保证可见性的?又是如何禁止指令重排的,它为什么不能实现原子性呢?
带着疑问,我们一起走进volatile的世界,探索它与可见性,有序性,原子性之间的爱恨情仇!
volatile如何保证可见性?
volatile保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见。
我们先通过之前写的一个小案例来感受一下什么是可见性问题:
【代码示例1】
public class Test {//是否停止 变量private static boolean stop = false;public static void main(String[] args) throws InterruptedException {//启动线程 1,当 stop 为 true,结束循环new Thread(() -> {System.out.println("线程 1 正在运行...");while (!stop) ;System.out.println("线程 1 终止");}).start();//休眠 1 秒Thread.sleep(1000);//启动线程 2, 设置 stop = truenew Thread(() -> {System.out.println("线程 2 正在运行...");stop = true;System.out.println("设置 stop 变量为 true.");}).start();}
}
输出:
线程 1 正在运行...
线程 2 正在运行...
设置 stop 变量为 true.
原因:
我们会发现,线程1运行起来后,休眠1秒,启动线程2,可即便线程2把stop设置为true了,线程1仍然没有停止,这个就是因为 CPU 缓存导致的可见性导致的问题。线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上执行,读取的 CPU 1 缓存中的 stop 变量仍然为 false,线程 1 一直在循环执行。

那这个问题怎么解决呢?很好解决!我们排volatile上场可以秒搞定,只需要给stop变量加上volatile修饰符即可!
【代码示例2】
//给stop变量增加volatile修饰符
private static volatile boolean stop = false;
输出:
线程 1 正在运行...
线程 2 正在运行...
设置 stop 变量为 true.
线程 1 终止
从结果中看,线程1成功的读取到了线程而设置为true的stop变量值,解决了可见性问题。那volatile到底是什么让变量在多个线程之间保持可见性的呢?请看下图!

如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取,具体实现可总结为5步。
- 1️⃣在生成最低成汇编指令时,对volatile修饰的共享变量写操作增加Lock前缀指令,Lock 前缀的指令会引起 CPU 缓存写回内存;
- 2️⃣CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效;
- 3️⃣volatile 变量通过缓存一致性协议保证每个线程获得最新值;
- 4️⃣缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改;
- 5️⃣当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存。
volatile如何保证有序性?
在之前的学习我们了解到,为了充分利用缓存,提高程序的执行速度,编译器在底层执行的时候,会进行指令重排序的优化操作,但这种优化,在有些时候会带来 有序性 的问题。
那何为有序性呢?我们可以通俗理解为:程序执行的顺序要按照代码的先后顺序。 当然,之前我们还说过发生有序性问题时,我们可以通过给变量添加volatile修饰符进行解决。
首先,我们来回顾一下之前写的一个关于有序性问题的测试类。
【代码示例1】
int a = 1;(1)
int b = 2;(2)
int c = a + b;(3)
上面的这段代码中,c变量依赖a,b的值,因此,在编译器优化重排时,c肯定会在a,b赋值以后执行,但a,b之间没有依赖关系,可能会发生重排序,但这种重排序即便到了多线程中依旧不会存在问题,因为即便重排对执行结果也无影响。
但有些时候,指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,我们继续看下面这段代码:
【代码示例2】
public class Test {private static int num = 0;private static boolean ready = false;//禁止指令重排,解决顺序性问题//private static volatile boolean ready = false;public static class ReadThread extends Thread {@Overridepublic void run() {while (!Thread.currentThread().isInterrupted()) {if (ready) {//(1)System.out.println(num + num);//(2)}System.out.println("读取线程...");}}}public static class WriteRead extends Thread {@Overridepublic void run() {num = 2;//(3)ready = true;//(4)System.out.println("赋值线程...");}}public static void main(String[] args) throws InterruptedException {ReadThread rt = new ReadThread();rt.start();WriteRead wr = new WriteRead();wr.start();Thread.sleep(10);rt.interrupt();System.out.println("rt stop...");}
}
我们定义了2个线程,一个用来求和操作,一个用来赋值操作,因为定义的是成员变量,所以代码(1)(2)(3)(4)之间不存在依赖关系,在运行时极可能发生指令重排序,如将(4)在(3)前执行,顺序为(4)(1)(3)(2),这时输出的就是0而不是4,但在很多性能比较好的电脑上,这种重排序情况不易复现。
这时,我们给ready 变量添加一个volatile关键字,就成功的解决问题了。
volatile关键字可以禁止指令重排的原因主要有两个!
一、3 个 happens-before 规则的实现
- 对一个 volatile 变量的写 happens-before 任意后续对这个 volatile 变量的读;
- 一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- happens-before 传递性,A happens-before B,B happens-before C,则 A happens-before C。
二、内存屏障
变量声明为 volatile 后,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令),为了实现volatile 内存语义,volatile 变量的写操作,在变量的前面和后面分别插入内存屏障;volatile 变量的读操作是在后面插入两个内存屏障。
具体屏障规则:
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障;
- 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障;
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障;
- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
屏障说明:
- StoreStore:禁止之前的普通写和之后的 volatile 写重排序;
- StoreLoad:禁止之前的 volatile 写与之后的 volatile 读/写重排序;
- LoadLoad:禁止之后所有的普通读操作和之前的 volatile 读重排序;
- LoadStore:禁止之后所有的普通写操作和之前的 volatile 读重排序。
OK,知道了这些内容之后,我们再回头看代码示例2中,增加了volatile关键字后的执行顺序,在赋值线程启动后,执行顺序会变成(3)(4)(1)(2),这时打印的结果就为4啦!
volatile为什么不能保证原子性?
我们讲完了volatile修饰符保证可见性与有序性的内容,接下来我们思考另外一个问题,它能够保证原子性吗?为什么?我们依旧通过一段代码去证明一下!
【代码示例3】
public class Test {//计数变量static volatile int count = 0;public static void main(String[] args) throws InterruptedException {//线程 1 给 count 加 10000Thread t1 = new Thread(() -> {for (int j = 0; j <10000; j++) {count++;}System.out.println("thread t1 count 加 10000 结束");});//线程 2 给 count 加 10000Thread t2 = new Thread(() -> {for (int j = 0; j <10000; j++) {count++;}System.out.println("thread t2 count 加 10000 结束");});//启动线程 1t1.start();//启动线程 2t2.start();//等待线程 1 执行完成t1.join();//等待线程 2 执行完成t2.join();//打印 count 变量System.out.println(count);}
}
我们创建了2个线程,分别对count进行加10000操作,理论上最终输出的结果应该是20000万对吧,但实际并不是,我们看一下真实输出。
输出:
thread t1 count 加 10000 结束
thread t2 count 加 10000 结束
14281
原因:
Java 代码中 的 count++并非原子的,而是一个复合性操作,至少需要三条CPU指令:
- 指令 1:把变量 count 从内存加载到CPU的寄存器
- 指令 2:在寄存器中执行 count + 1 操作
- 指令 3:+1 后的结果写入CPU缓存或内存
即使是单核的 CPU,当线程 1 执行到指令 1 时发生线程切换,线程 2 从内存中读取 count 变量,此时线程 1 和线程 2 中的 count 变量值是相等,都执行完指令 2 和指令 3,写入的 count 的值是相同的。从结果上看,两个线程都进行了 count++,但是 count 的值只增加了 1。这种情况多发生在cpu占用时间较长的线程中,若单线程对count仅增加100,那我们就很难遇到线程的切换,得出的结果也就是200啦。
要想解决也很简单,利用 synchronized、Lock或者AtomicInteger都可以,我们在后面的文章中会聊到的,请继续保持关注哦!
结尾彩蛋
如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

相关文章:
走进volatile的世界,探索它与可见性,有序性,原子性之间的爱恨情仇!
写在开头 在之前的几篇博文中,我们都提到了 volatile 关键字,这个单词中文释义为:不稳定的,易挥发的,在Java中代表变量修饰符,用来修饰会被不同线程访问和修改的变量,对于方法,代码…...
python从入门到精通(十五):python爬虫完整学习大纲
一、基础知识 爬虫的基本概念和工作原理。 HTTP 协议和网页结构。 Python 爬虫开发的基础库,如 requests、BeautifulSoup 等。 常见的反爬虫机制和应对方法。 二、爬虫逆向的技术 代理服务器和 IP 封锁突破。 用户代理和请求头模拟。 JavaScript 解析和执行。 验证码…...
为什么JDK8.0 之后允许接口定义静态方法和默认方法呢?
为什么JDK8.0 之后允许接口定义静态方法和默认方法呢? 因为它违反了接口作为一个抽象标准定义的概念。** 静态方法:因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态…...
如何通过生成式AI增强人类的创造力
如何通过生成式AI增强人类的创造力 概述: 生成式AI(人工智能),能创建新的文本、图像和视频内容,不仅仍有成为取代许多工作岗位的潜力,但其最大的机遇在于增强人类创造力,助力商业和政府克服创新…...
力扣111---二叉树的最小深度(简单题,Java,递归+非递归)
目录 题目描述: (递归)代码: (非递归、层次遍历)代码: 题目描述: 给定一个二叉树,找出其最小深度。 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 说…...
C#处理文件
目录 1.管理文件2.管理驱动器3.管理目录4.管理路径5.获取文件信息6.控制如何处理文件 1.管理文件 C# 中使用 File 类可以进行文件的读取、写入和删除操作。File 类提供了多个静态方法来处理文件,如 File.Exists() 用于检查文件是否存在,File.ReadAllTex…...
git |常用命令
git 命令 非常常用 主流的仓库管理服务器,svn 和git 接下来,介绍git 操作(自用 先讲一个简单的demo 流程 环境:centos、git #先创建一个本地 git 文件夹 mkdir test && cd ./test #写一个README.md #echo “# 张不大 的de…...
力扣100热题:两、三、四数之和,哈希+数组+双指针+排序
目录 一、两数之和 二、两数之和 II - 输入有序数组 三、两数之和 III - 数据结构设计 四、两数之和 IV - 输入 BST(二叉搜索树) 五、三数之和 六、四数之和 一、两数之和 题目:1. 两数之和 参考力扣题解:. - 力扣&#x…...
国外visa卡怎么办理,可充ChatGPTPLUS、Claude、Midjourney
很多小伙都在使用ChatGPT,但是想充值ChatGPTPLUS缺需要国外的visa卡,拿自己的银联卡,尝试了好多次还是不行,其实用一张国外的visa卡几分钟就可以升级好 办理国外visa卡,点击获取 国外的visa卡,具体要看你…...
【Web】记录[长城杯 2022 高校组]b4bycoffee题目复现
目录 前言 环境准备 简单分析 EXP 前言 本地jar包运行打通了,远程500,nss靶机有问题,换了bugku就可( 主要记录下做题过程,纯菜狗,小白文 环境准备 这次附件给的jar包是可执行jar,不是可依赖jar&…...
C++ 多路音频pcm混音算法
1、均值化混音算法 不适合商用,声音的损失比较大,不建议用,建议用第二种声音混音 short remix(short pcm1,short pcm2){ int value pcm1 pcm2; return (short)(value/2) } 2、归一化混音算法 输入数据为48Khz-2-16bit音频数据 方法&#…...
Golang 泛型定义类型的时候前面 ~ 代表什么意思
先看代码,定义一个简单的泛型 c1 里面一个 int ,定义一个函数goods 下面 main函数进行调用, 如果直接传int 类型是不会报错的,但是如果传自定义类型的b就会报错。 type c1 interface {int }func goods[T c1](a T) {fmt.Println(a) }type myint intfunc …...
泽众云真机-机型支持ADB调试功能即将上线
最近云真机平台在线客服,收到很多咨询关于ADB调试功能,什么时候能更新?据小编所知,正在升级之中,有一块专门为了解决ADB调试功能提前准备,升级网络硬件设备,目前平台的功能已开发完成࿰…...
基于springboot的购物商城管理系统
1.项目简介 1.1 用户简介 用户主要分为管理员和用户端: 管理员: 管理员可以对后台数据进行管理、拥有最高权限、具体权限有登录后进行首页轮播图的配置管理、商品的配置、新品家具商城的配置管理、、家具商城分类管理配置、家具商城详情商品管理、用户…...
uni-app开发特点和开发流程
uni-app是一个基于Vue.js框架的跨平台应用开发框架,通过一套代码可以同时运行在多个平台上,包括iOS、Android、H5等。它采用了基于流布局的页面渲染机制,可以自动适配不同平台的屏幕尺寸和分辨率。uniapp官网:https://uniapp.dclo…...
Sentinel篇:线程隔离和熔断降级
书接上回:微服务:Sentinel篇 3. 隔离和降级 限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。 而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离…...
HTML静态网页成品作业(HTML+CSS)——家乡广州介绍设计制作(5个页面)
🎉不定期分享源码,关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 🏷️本套采用HTMLCSS,未使用Javacsript代码,共有5个页面。 二、作品演示 三、代…...
【Java IO流】缓冲流和对象流的解析和应用实例
目录 前言 一、缓冲流 四种方式拷贝文件的用时对比 二、对象流 1. 使用对象流写入对象到本地文件 2. 使用对象流读取对象数据 总结 前言 【File文件管理及IO流(基本流)】http://t.csdnimg.cn/uG5Ff 该篇博客中,介绍了学习高级流需要的…...
鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Select)
提供下拉选择菜单,可以让用户在多个选项之间选择。 说明: 该组件从API Version 8开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。 子组件 无 接口 Select(options: Array<SelectOption>) 参数:…...
mysql将一个表另存为新表,同时复制索引、约束、主键等信息
使用 SHOW CREATE TABLE 语句获取原表的创建语句: SHOW CREATE TABLE 原表名;将 原表名 替换为要复制的原始表的名称。 此语句将返回一个包含原表完整创建语句的结果集。创建语句包括表的结构、列定义、索引、约束、主键等所有信息。 复制结果集中的创建语句&…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...
WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...
10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
