volatile
什么是volatile
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
好,开始讲大家看不懂的东西了!
volatile有三大特性:
- 保证可见性
- 不保证原子性
- 有序性
傻了吧,这都是些什么东西啊?别着急,我们一个一个来。
在学习volatile之前,我们先了解一下JMM。什么又是JMM?我只知道JVM。这他妈是啥东西啊?
JMM:java内存模型。jmm是一种抽象的概念,并不真实存在,它描述的是一种规范,通过这种规范定义了程序中的各个变量的访问形式。(仔细读,还是能读懂的)
JMM关于同步的规定(仔细读):
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
知道看不懂,开始白话文解释!

JVM我们的java虚拟机运行程序的时候,是以线程为最小刻度的。而每个线程创建的时候,jvm就会为这个线程创建一个工作内存,该工作内存是私有的,只能被当前线程所访问。
而JMM内存模型中规定:所有的变量都储存在主内存中,所有线程都能访问,但线程对变量的任何操作(读取赋值等)都必须在工作内存中进行,首先要将主内存中的变量拷贝到自己的工作内存中,然后才能对变量进行操作,操作完成后再将变量写回主内存中。
这里我们发现了一个问题:
先试想这样一个场景:现在有一个商品只剩下最后一个,如果两个线程同时进来抢,拿到了一个变量:int a = 1;(商品的数量) 这时候这个int a = 1;会拷贝出两份,分别存在于线程1的工作内存和线程2的工作内存。 我们知道,不同线程间是无法访问对方的工作内存的。
这个时候线程1 跑得快一点抢到了最后一个商品,把int a 的值减去1了,然后通知快递部门上门来取货,把这最后一个商品拿走发货,然后把最新的a的值返回给主内存。现在主内存int a 的值等于0。
但对于线程2来说,它现在只看自己的工作内存,不看主内存,对于线程2来说,int a 的值现在还是1。所以它就觉得它也抢到了商品,其实这时主内存中的int a已经是0了,已经没有商品了。这时线程2把自己工作内存的int a 的值减去1,然后通知快递部门来取货,快递来了发现你他妈的商品都卖完了我来取个啥?
上面就出现了超卖的情况,其根本原因就是:多个线程之间不能知道对方的对共享变量的执行情况,大家都是盯着自己的东西在做事。就像两个施工队在山的两边一起往中间打隧道,互相不知道对方的情况,最后两个隧道在山的中间完美错过。
好!那么有没有一个办法,只要有一个线程修改了主内存的变量的值以后,其他的线程能马上知道并获取到最新的值呢?
volatile的可见性
一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。
先看看没有使用volatile关键字的情况:
1.编写一个类,模拟售卖商品的过程,商品数量我们初始化为 Int a = 1;
class Shop{int a = 1;public void saleOne(){this.a = a-1;}
}
2.测试类
public static void main(String[] args) {Shop shop = new Shop();new Thread(()->{System.out.println("线程A初始化");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}shop.saleOne();System.out.println("线程A购买商品完成,剩余商品量:"+shop.a);},"线程A").start();while (shop.a == 1){}System.out.println("主线程,剩余商品量:"+shop.a);}
这里有两个线程,线程A和主线程。 程序启动的时候:
- 线程A和主线程分别把shop 对象拷贝一份到自己的工作内存,对于这两个线程来说,shop 对象里的int a 属性的值都是1;
- 启动线程A
- 线程A等待3秒,再此3秒期间,主线程已经运行到while (shop.a == 1)这行代码,因为判断为true所以一直阻塞。
- 3秒过后,线程A调用方法,把自己工作内存中的int a 减 1,并把最新的int a = 0发送到主内存。
- 但我们发现,主线程还是一直处于阻塞的状态,对于主线程来说,它不知道int a 的值已经变为0,对主线程来说现在int a 的值还是自己工作内存中的1,所以 while (shop.a == 1)的判断永远为True。不会执行最后一行代码System.out.println(“主线程,剩余商品量:”+shop.a);

我们加上volatile关键字
class Shop{volatile int a = 1;public void saleOne(){this.a =a-1;}
}
测试代码不变
结果:

- 线程A和主线程分别把shop 对象拷贝一份到自己的工作内存,对于这两个线程来说,shop 对象里的int a 属性的值都是1;
- 启动线程A
- 线程A等待3秒,再此3秒期间,主线程已经运行到while (shop.a == 1)这行代码,因为判断为true所以一直阻塞。
- 3秒过后,线程A调用方法,把自己工作内存中的int a 减 1,并把最新的int a = 0发送到主内存。
- 由于volatile的可见性,此时对于主内存来说 int a的值已经由1变为了0, while (shop.a == 1)判断为False。程序就继续往下走,打印出了最新的int a的值:0。这时候商品数量为0之后,我们就不会再出现超卖的情况了
volatile的原子性(不保证)
原子性什么意思呢?
一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。
大白话翻译:同一个方法,在一个线程没有执行完之前,其他线程必须给我等着。等我执行完了再放第二个线程进来。以免线程1的操作被线程2给覆盖了。比如synchronized,就保证了原子性。
给我们的Shop类创建一个增加商品库存的方法(每调一次addGoods方法,Int a就+1):
class Shop{volatile int a = 1;public void addGoods(){a++;}public void saleOne(){this.a =a-1;}}
此时int a商品数量是加了volatile 修饰的,保证了不同线程之间的可见性!
测试:
public static void main(String[] args) {Shop shop = new Shop();for(int i = 0; i < 20;i++){new Thread(()->{shop.addGoods();}).start();}//保证所有20个线程都跑完,只剩下2个线程(主线程和GC线程)的时候代码才继续往下走//其中 Thread.yield() 方法表示主线程不执行,让给其他线程执行while (Thread.activeCount() >2){Thread.yield();}System.out.println("如果保证了原子性,应该的结果是本来的1+20 = 21,但实际的值:"+shop.a);}
- 开20个线程去执行addGoods()方法
- 最后主线程把int a 的数值打印出来
结果让我们大失所望,每次执行程序得到的结果都不一样

这里我们知道,volatile不能保证程序的原子性。那为什么呢?
首先明确一点 a++操作不是原子性,它有三步:
- 获取主内存的当前值到自己的工作内存
- 进行+1操作
- 把最新值写回到主内存
尚且a++都不是原子操作,那我们平时的业务代码是不是更长,花的时间也更多?被其他线程覆盖的机会是不是也更大?
好,现在我们来看看上面的20个线程的例子怎么来分析!
- 加入现在有l两个线程几乎同时进入到addGoods()方法里面。
- 对于A,B两个线程而言,现在int a 的值都拷贝到各自的工作内存中,值都=1。
- 现在线程A开始执行a++操作,底层获取到当前值,然后+1,得到值为2,准备把最新值写入到主内存
- 这个时候由于多线程的机制,线程A在写入主内存之前被挂起了!
- 线程B开始执行了,成功的把int a 从加到2,写入主内存,现在主内存的值是2
- 线程A现在又被唤起,完成第3步没有完成的操作,把线程A自己工作内存中的2写入到主内存。
- 但现在主内存本来就是2,线程A由于在执行底层的++操作,没有机会去读取到最新的值。
以上!就是整个代码运行流程,解释了volatile为什么不能保证原子性。我知道很多同学还是没看懂,别急,我是红色文章最后会有更直观的例子(单例模式中的线程安全问题),一看就明白了
现在我们想一想,怎么解决volatile这个缺点呢?怎么实现原子性?
- 1,在addGoods方法加同步锁synchronized
- 2, AtomicInteger原子类
我们讲第二种:
修改我们的Shop类
class Shop{AtomicInteger atomicInteger = new AtomicInteger(1);public void addGoodsByAtomic(){atomicInteger.getAndIncrement();}
- 初始化原子类值为1
- 创建新方法,方法体让原子类自增1,整个过程是原子性的。
测试:
public static void main(String[] args) {Shop shop = new Shop();for(int i = 0; i < 20;i++){new Thread(()->{shop.addGoodsByAtomic();}).start();}while (Thread.activeCount() >2){Thread.yield();}System.out.println("如果保证了原子性,应该的结果是本来的1+20 = 21,但实际的值:"+shop.atomicInteger);}
结果正确:

为什么原子类保证了原子性?这个设涉及到CAS锁。
volatile的有序性(禁止指令重排)(了解)
我们写的java代码,为了提高性能,在编译器和处理器中往往会进行指令重排,例如我写的某一行代码在23行,当经过编译过后这行代码在150行。
多线程环境中,由于编译器重排的原因,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
简单来说volatile避免了指令重排,也就避免了多线程中可能产生的问题。
volatile的运用场景(重点)
单例模式:
public class type3 {private static type3 type;private type3(){}private static type3 getInstance(){if(type == null){type = new type3();}return type;}}
public class type4 {private static type4 type;private type4(){}private static synchronized type4 getInstance(){if(type == null){type = new type4();}return type;}}
但synchronized把整个方法都锁了,在高并发的情况下,太重了。并发性下降了,吞吐量下降了。
所以出现了效率最高,也安全的单例模式写法:双重检查!
public class type5 {private static type5 type;private type5(){}private static type5 getInstance(){if(type == null){synchronized(type5.class){if(type == null){type = new type5();}}}return type;}}
大家觉得上面的代码有没有什么问题?
我来梳理一下。
- 现在有A B两个线程同时进来,都通过了第一次检查。现在到达了synchronized同步锁外面
- A线程运气好,被先放进去了,再次检查发现type确实为null,好,放行
- A线程new了一个实例出来,这是把这个最新的实例返回给主内存,主内存的对象变量从Null变为有值
- A线程完成,B线程被放synchronized开始进行B线程的第二次检查
- 但由于type5 变量没有volatile修饰,所以线程B不能马上获取到最新的值,它不知道现在对象已经被new出来了,在线程B自己的工作内存了对象依然为null。
- B线程通过第二次检查,又new了一个对象出来。单例的目标没有达成,上面的代码失败。
所以我们要给变量加上volatile关键字:
private static volatile type5 type;
查发现type确实为null,好,放行
3. A线程new了一个实例出来,这是把这个最新的实例返回给主内存,主内存的对象变量从Null变为有值
4. A线程完成,B线程被放synchronized开始进行B线程的第二次检查
5. 但由于type5 变量没有volatile修饰,所以线程B不能马上获取到最新的值,它不知道现在对象已经被new出来了,在线程B自己的工作内存了对象依然为null。
6. B线程通过第二次检查,又new了一个对象出来。单例的目标没有达成,上面的代码失败。
所以我们要给变量加上volatile关键字:
private static volatile type5 type;
相关文章:
volatile
什么是volatile volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更…...
JAVA:实现Excel和PDF上下标
1、简介 最近项目需要实现26个小写字母的上下标功能,自己去网上找了所有Unicode的上下标形式,缺少一些关键字母,顾后面考虑自己创建上下标字体样式,以此来记录。 2、Excel Excel本身是支持上下标,我们可以通过Excel单元格的样式来设置当前字体上下标,因使用的是POI的m…...
AI写稿软件,最新的AI写稿软件有哪些
写作已经成为各行各业无法绕开的重要环节。不论是企业的广告宣传、新闻媒体的报道、还是个人自媒体的内容创作,文字都扮演着不可或缺的角色。随着信息的爆炸式增长,写作的需求也不断攀升,这使得许多人感到困扰。时间不够用、创意枯竭、写作技…...
干货:数据仓库基础知识(全)
1、什么是数据仓库? 权威定义:数据仓库是一个面向主题的、集成的、相对稳定的、反映历史变化的数据集合,用于支持管理决策。 1)数据仓库是用于支持决策、面向分析型数据处理; 2)对多个异构的数据源有效集…...
二分搜索简介
概念: 二分搜索算法(Binary Search)是一种高效的搜索算法,用于在有序数组中查找特定元素的位置。它的基本思想是将数组分为两部分,通过比较目标值与数组中间元素的大小关系,确定目标值可能存在的区间&…...
虚拟车衣VR云展厅平台扩大了展览的触达范围
传统展厅主要是以静态陈列的形式来传达内容,主要的展示形式有图片、视频等,具有一定的局限性,体验感较差,客户往往不能深入地了解信息和细节内容。 VR全景看车是通过虚拟现实技术实现逼真的汽车观赏和试乘体验。消费者可以通过智能…...
云部署家里的服务器
1.固定静态ip 查看ip地址,en开头的 ifconfig查看路由器ip,via开头的 ip route修改配置文件 cd /etc/netplan/ #来到这个文件夹 sudo cp 01-network-manager-all.yaml 01-network-manager-all.yaml.bak #先备…...
【利用冒泡排序的思想模拟实现qsort函数】
1.qsort函数 1.1qsort函数的介绍 资源来源于cplusplus网站 1.2qsort函数的主要功能 对数组的元素进行排序 对数组中由 指向的元素进行排序,每个元素字节长,使用该函数确定顺序。 此函数使用的排序算法通过调用指定的函数来比较元素对,并将指…...
[plugin:vite:css] [sass] Undefined mixin.
前言: vite vue3 TypeScript环境 scss报错: [plugin:vite:css] [sass] Undefined mixin. 解决方案: 在vite.config.ts文件添加配置 css: {preprocessorOptions: {// 导入scss预编译程序scss: {additionalData: use "/resources/_ha…...
【论文阅读】大语言模型中的文化道德规范知识
摘要: 在已有的研究中,我们知道英语语言模型中包含了类人的道德偏见,但从未有研究去检测语言模型对不同国家文化的道德差异。 我们分析了语言模型包含不同国家文化道德规范的程度,主要针对两个方面,其一是看语言模型…...
51单片机实训项目之产品数量计数器
/********************************************************************************* * 【实验平台】: QX-MCS51 单片机开发板 * 【外部晶振】: 11.0592mhz * 【主控芯片】: STC89C52 * 【编译环境】: Keil μVisio3 * 【程序…...
Scala第七章节
Scala第七章节 scala总目录 章节目标 掌握继承和抽象类相关知识点掌握匿名内部类的用法了解类型转换的内容掌握动物类案例 1. 继承 1.1 概述 实际开发中, 我们发现好多类中的内容是相似的(例如: 相似的属性和行为), 每次写很麻烦. 于是我们可以把这些相似的内容提取出来单…...
C语言进程的相关操作
C语言进程的相关操作 进程简介 每个进程都有一个非负整数形式到的唯一编号,即PID(Process Identification,进程标识)PID在任何时刻都是唯一的,但是可以重用,当进程终止并被回收以后,其PID就可…...
数据结构学习系列之链式栈
链式栈:即:栈的链式存储结构;分析:为了提高程序的运算效率,应采用头插法和头删法;进栈: int push_link_stack(stack_t *link_stack,int data) {if(NULL link_stack){printf("入参合理性检…...
too many session files in /var/tmp
Linux中Too many open files 问题分析和解决_e929: too many viminfo temp files-CSDN博客...
【7.0】打开未知来源安装应用
默认打开未知来源安装应用 frameworks\base\packages\SettingsProvider\res\values\defaults.xml <bool name"def_install_non_market_apps">false</bool>...
安装ipfs-swarm-key-gen
安装ipfs-swarm-key-gen Linux安装go解释器安装ipfs-swarm-key-gen Linux安装go解释器 https://blog.csdn.net/omaidb/article/details/133180749 安装ipfs-swarm-key-gen # 编译ipfs-swarm-key-gen二进制文件 go get -u github.com/Kubuxu/go-ipfs-swarm-key-gen/ipfs-swarm…...
BASH shell脚本篇5——文件处理
这篇文章介绍下BASH shell中的文件处理。之前有介绍过shell的其它命令,请参考: BASH shell脚本篇1——基本命令 BASH shell脚本篇2——条件命令 BASH shell脚本篇3——字符串处理 BASH shell脚本篇4——函数 在Bash Shell脚本中,可以使用…...
ElementUI之首页导航及左侧菜单(模拟实现)
目录 编辑 前言 一、mockjs简介 1. 什么是mockjs 2. mockjs的用途 3. 运用mockjs的优势 二、安装与配置mockjs 1. 安装mockjs 2. 引入mockjs 2.1 dev.env.js 2.2 prod.env.js 2.3 main.js 三、mockjs的使用 1. 将资源中的mock文件夹复制到src目录下 2. 点击登…...
Java开源工具库使用之Lombok
文章目录 前言一、常用注解1.1 AllArgsConstructor/NoArgsConstructor/RequiredArgsConstructor1.2 Builder1.3 Data1.4 EqualsAndHashCode1.5 Getter/Setter1.6 Slf4j/Log4j/Log4j2/Log1.7 ToString 二、踩坑2.1 Getter/Setter 方法名不一样2.2 Builder 不会生成无参构造方法2…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...
Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
