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…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...

css实现圆环展示百分比,根据值动态展示所占比例
代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...
Robots.txt 文件
什么是robots.txt? robots.txt 是一个位于网站根目录下的文本文件(如:https://example.com/robots.txt),它用于指导网络爬虫(如搜索引擎的蜘蛛程序)如何抓取该网站的内容。这个文件遵循 Robots…...