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…...
uboot启动流程涉及reset函数
一. uboot启动流程中函数 之前了解了uboot链接脚本文件 u-boot.lds。 从 u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的 _start。 本文了解 一下,uboot启动过程中涉及的 reset 函数。本文继上一篇文章学习,地址如下ÿ…...
端口被占用怎么解决
第一步:WinR 打开命令提示符,输入netstat -ano|findstr 端口号 找到占用端口的进程 第二步: 杀死使用该端口的进程,输入taskkill /t /f /im 进程号( !!!注意是进程号,不…...
python reportlab 生成多页pdf
多页 from reportlab.pdfgen import canvas from reportlab.platypus import (SimpleDocTemplate, Paragraph, PageBreak, Image, Spacer, Table, TableStyle) from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY from reportlab.lib.styles import P…...
word 多级目录的问题
一、多级标题自动编号 --> 制表符 -> 空格 网址: 【Word技巧】2 标题自动编号——将多级列表链接到样式 - YouTube 二、多级列表 --> 正规形式编号 网址:Word 教学 - 定框架:文档格式与多级标题! - YouTube 三、目…...
python使用mitmproxy和mitmdump抓包之拦截和修改包(四)
我认为mitmproxy最强大的地方,就是mitmdump可以结合python代理,灵活拦截和处理数据包。 首先,mitmdump的路径如下:(使用pip3 install mitmproxy安装的情况,参考我的文章python使用mitmproxy和mitmdump抓包…...
邓俊辉《数据结构》→ “2.6.5 二分查找(版本A)”之“成功查找长度”递推式推导
【问题描述】 邓俊辉的《数据结构(C语言版)(第3版)》(ISBN:9787302330646)中,开始于第48页的“2.6.5 二分查找(版本A)”内容在第50页详述了“成功查找长度”的…...
Linux文件查找,别名,用户组综合练习
1.文件查看: 查看/etc/passwd文件的第5行 [rootserver ~]# head -5 /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologi…...
【MATLAB第77期】基于MATLAB代理模型算法的降维/特征排序/数据处理回归/分类问题MATLAB代码实现【更新中】
【MATLAB第77期】基于MATLAB代理模型算法的降维/特征排序/数据处理回归/分类问题MATLAB代码实现 本文介绍基于libsvm代理模型算法的特征排序方法合集,包括: 1.基于每个特征预测精度进行排序(libsvm代理模型) 2.基于相关系数corr的…...
第三章 图标辅助元素的定制
第三章 图标辅助元素的定制 1.认识图表常用的辅助元素 图表的辅助元素是指除了根据数据绘制的图形之外的元素,常用的辅助元素包括坐标轴、标题、图例、网格、参考线、参考区域、注释文本和表格,它们都可以对图形进行补充说明。 上图中图表常用辅…...
【前端】ECMAScript6从入门到进阶
【前端】ECMAScript6从入门到进阶 1.ES6简介及环境搭建 1.1.ECMAScript 6简介 (1)ECMAScript 6是什么 ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在2015年6月正式发布了。它的目标ÿ…...
山东建设公司网站/360站长平台链接提交
先需求分析一下---传统的web应用是在线应用,这其实也是web的特色,对于PC时代问题并不大,但到了移动互联网时代,设备终端位置不再固定,依赖无线信号,网络的可靠性变得更低。比如:在火车上&#x…...
做视频可以赚钱的网站/广告策划书
MySQL存储引擎分类及操作介绍发布时间:2020-06-01 15:12:52来源:51CTO阅读:133作者:三月不知道大家之前对类似MySQL存储引擎分类及操作的文章有无了解,今天我在这里给大家再简单的讲讲。感兴趣的话就一起来看看正文部分…...
天津网站建设交易/引流用什么话术更吸引人
7、今有三分之一,五分之二。问合之得几何? 答曰:十五分之十一。 译文: 设有分数13、25,问相加得多少? 答:1115。...
佛山响应式网站建设/知乎推广合作
mybatis缓存:在内存中临时存储数据,速度快,可以减少数据库的访问次数 经常需要查询,不经常修改的数据,不是特别重要的数据都适合于存储到缓存中 一级缓存 默认开启,是SqlSession的缓存,SqlSe…...
成都今日各区疫情情况/ seo won
BossAnim 项目地址:Alex-Cin/BossAnim简介:仿 boss 直聘,查看用户头像博客地址 http://www.jianshu.com/p/eadd1a764720BossAnim 博客地址 http://www.jianshu.com/p/eadd1a764720...
功能型网站 设计/青岛网站排名公司
在看Thomas Kyte大师的《Oracle Database 9i/10g/11g编程艺术》的配置环境一节有一个login.sql脚本define _editorviset serveroutput on size 1000000set trimspool onset long 5000set linesize 100set pagesize 9999column plan_plus_exp format a80column global_name new_…...