volatile详解
liebian365 2025-01-04 21:17 210 浏览 0 评论
前言
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
相信很多人都用过volatile这个关键字,也知道它的妙用,但是其底层原理是否知晓呢?通过这篇文章就一目了然了。 在了解volatile之前CPU多及缓存架构和JMM内存模型,如果不了解的在我的其他文章里面有讲到这两点。
JMM数据原子操作
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入到工作内存
- use(使用):从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入):将store过去的变量赋值给主内存中的变量
- lock(锁定):将主内存变量加锁,标志变量为线程独享状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可对该变量进行加锁操作
volatile可见性分析
public class VolatileVisibilityTest {
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("wait data!");
while (flag) {
}
System.out.println("data change!");
}).start();
Thread.sleep(3000);
new Thread(() -> {
System.out.println("updating data");
flag = false;
System.out.println("updated data");
}).start();
}
}
// flag不加volatile进行修饰时,程序运行结果,并且第一个线程一直在死循环
wait data!
updating data
updated data
// flag加volatile进行修饰时,程序运行结果
wait data!
updating data
updated data
data change!
这里很直观地感受到volatile的效果:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值。 当flag加了volatile关键字修饰时,线程2修改完flag的值之后线程1能感受到flag变量被修改了,为什么会这样子呢?下面结合JMM原子性操作对实现机制进行深入讲解。
- 声明一个变量flag = true,存储在主内存中
- 通过read方法读取变量flag=true(线程1、2)
- 通过load方法加载变量flag=true到各自线程的工作内存(线程1、2)
- 通过use方法使用本线程工作内存中变量flag=true(线程1进入死循环状态,直到flag变成了false)
- 线程2通过assign方法对本地变量flag进行计算并赋值flag=false
- 线程2准备通过store方法将本地变量flag=false写入主内存,此时会调用lock方法对这块内存区域的缓存进行锁定(缓存行锁定)。
- 通过lock对缓存行锁定之后,MESI总线嗅探机制监听到变量flag的值发生了变化,此时会对所有使用该变量线程工作内存中的flag变量进行失效,导致其他线程需要从2步骤开始重新从主内存获取flag变量(其他线程从主内存获取flag变量的时机需要等待缓存行解锁,如果此处不对缓存行进行锁定,那么其他线程去读取主内存时可能获取到的变量还是原来的值)。
- 线程2通过write方法对主内存变量flag进行修改(对flag变量重新赋值)
- 线程2调用unlock方法,对锁定的缓存行进行解锁,解锁完成之后其他线程从主内存重新获取变量flag=false。
volatile主要通过汇编lock前缀指令,它会锁定当前内存区域的缓存(缓存行),并且立即将当前缓存行数据写入主内存(耗时非常短),回写主内存的时候会通过MESI协议使其他线程缓存了该变量的地址失效,从而导致其他线程需要重新去主内存中重新读取数据到其工作线程中。
volatile原子性分析
volatile能保证原始数据类型赋值的原子性,无法保证复合操作的原子性。
public class VolatileAtomicTest {
private volatile int i = 0;
public void add() {
i++;
}
public static void main(String[] args) {
for (int t = 0; t < 10; t++) {
VolatileAtomicTest test = new VolatileAtomicTest();
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int k = 0; k < 1000; k++) {
test.add();
}
});
threads[i].start();
}
Arrays.stream(threads).forEach(th -> {
try {
th.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("第" + (t + 1) + "次执行结果:" + test.i);
}
}
}
第1次执行结果:8423
第2次执行结果:8734
第3次执行结果:7658
第4次执行结果:8368
第5次执行结果:10000
第6次执行结果:9748
第7次执行结果:10000
第8次执行结果:10000
第9次执行结果:10000
第10次执行结果:10000
上面程序运行最终结果都会小于等于10000,为什么会出现这种情况呢,罪魁祸首就是在并发情况下违背了原子性操作导致,接下来一步一步进行分析原由。
- 线程的运行有随机性,假设此时线程1、2、3都从主内存读取变量i=0到了各自线程的工作内存
- 线程1、2、3都开始通过assign对本地变量进行赋值操作,此时各自线程内都变量i的值都变成了1
- 线程2优先调用原子方法store进行变量的主内存回写操作,此时通过lock对缓存行(i的缓存)进行锁定,由于线程2优先抢到了锁,导致线程1、3不能将其工作内存的变量i写回到主内存,并且此时通过CPU总线MESI协议将其工作内存变量i的地址失效,导致线程1、3的本次计算结果丢失
- 同理,线程4后执行,读取到了线程2修改变量i的值为2之后执行计算,当有其他线程抢先lock写入内存的时候,线程4的计算结果也会丢失,在这些巧合之下导致i最终计算结果会小于等于10000。
只需要在add方法处加上synchronized关键字进行修改,保证add方法的原子性就能保证每次执行结果都是10000。synchronized会在后面的文章中讲到
volatile有序性分析
public class VolatileSerialTest {
static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
Set<String> resultSet = new HashSet<>();
Map<String, Integer> resultMap = new HashMap();
for (int i = 0; i < 2000000; i++) {
x = 0;
y = 0;
Thread one = new Thread(() -> {
int a = y; //语句1
x = 1; //语句2
resultMap.put("a", a);
});
Thread two = new Thread(() -> {
int b = x; //语句3
y = 1; //语句4
resultMap.put("b", b);
});
one.start();
two.start();
one.join();
two.join();
resultSet.add("a=" + resultMap.get("a") + ",b=" + resultMap.get("b"));
System.out.println(resultSet);
}
}
}
a与b的结构回有四种情况:[a=0,b=0, a=1,b=0, a=0,b=1, a=1,b=1]
- [a=0,b=0]:两个线程同时执行到语句1和与语句4,此时从主内存获取到变量x、y值为0
- [a=1,b=0]:线程2先执行语句3、4,此时读取从主内存获取到变量x值为0,并为变量b赋值0,之后赋y为1,并将=1写入到主内存,之后线程1执行到语句1,从主内存获取y=1赋值给a
- [a=0,b=1]:线程1先执行语句1、2,此时读取从主内存获取到变量y值为0,并为变量a赋值0,之后赋x为1,并将=1写入到主内存,之后线程2执行到语句3,从主内存获取x=1赋值给b
- 语句1和语句2进行了指令重排,导致语句1优先与语句2先执行,语句3、4同理,此时主内存中x、y都变成了1,之后a、b赋值的时候都取到了1
当x、y通过volatile进行修饰时
static volatile int x = 0, y = 0;
执行的结果就只有三种情况了[a=0,b=0, a=1,b=0, a=0,b=1]
volatile修饰变量之后会禁止指令重排,语句1一定在语句2之前执行,语句3一定会在语句4之前执行。
汇编指令查看
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:PrintAssembly -XX:CompileCommand=compileonly,*VolatileSerialTest.main
volatile重排序规则表(针对编译器重排序):
是否能重排序 | 第二个操作 | ||
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | 是 | 是 | 否 |
volatile读 | 否 | 否 | 否 |
volatile写 | 是 | 否 | 否 |
上面的例子符合第二个volatile写第一个普通写规则,编译器不会进行重排序。
内存屏障(针对处理器重排序):
为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
内存屏障 | 第二个操作 | |||
第一个操作 | 普通读 | 普通写 | volatile读同步块入口 | volatile写同步块出口 |
普通读 | LoadStore | |||
普通写 | StroeStroe | |||
volatile读同步块入口 | LoadStore | LoadStore | LoadStore | LoadStore |
volatile写同步块出口 | StroeLoad | StroeLoad |
相关推荐
- 深度解密epoll 如何工作的?(epoll基本处理流程)
-
epoll...
- 大乐透第19082期:头奖开出7注1000万分落六地 奖池41亿元
-
2019年7月17日晚开奖的体彩超级大乐透第19082期开奖号码为:前区06、18、20、21、31,后区03、04。本期大乐透前区号码五区比为1:0:3:0:1,二区和四区号码没有给出。当期前区和值...
- 【开奖】4月27日周六:福彩、体彩(2021年4月27日体彩开奖结果)
-
4月27日开奖福彩3D第2019110期:61222选5第2019110期:0812202122排列3第19110期:303排列5第19110期:30305大乐透第19047期:0304...
- “红狒狒”落户哈尔滨铁路局(哈尔滨铁路红肠)
-
这几天,“红人”“红狒狒”在牡丹江机务段可引起了不小的轰动,众粉丝争相与其拍照留念,在该段人气爆棚!“红狒狒”到底何许人也?“红狒狒”,中文名:和谐3D型电力机车;绰号:红狒狒、番茄;制造商:大连机...
- 2D、3D、2.5D,做游戏还是搞噱头?玩家都晕了
-
前言游戏类型就像某种潮流,一种流行罢,另一种接棒成为主流。前两年的新作大多以“开放世界”为标签,在追求纯沙盒的过程中打造出一些细致的分类,比如说“类GTA沙盒”。诚然,纯碎的沙盒游戏并不多见,业内只有...
- 《战神4》PC版宣传片发布 GTX 1070即可60帧畅玩
-
在今年10月的时候索尼PlayStation官方正式宣布圣莫尼卡2018年的《战神4》将于2022年1月14日推出PC版本,官方在今天公布了一段PC版宣传片,并且公开了游戏的配置需求。下面让我们一起来...
- 男星深情好丈夫形象崩塌,半夜搂美女坐大腿,举止亲密
-
近日,于晓光被拍到深夜在酒吧玩,结束后与一名女子一起上车离开。上车后,女子直接坐在了他腿上,他也顺势搂着美女,美女满脸笑容地坐在他腿上玩手机离开。可能有人会好奇,于晓光是谁呢?于晓光是韩国艺人秋瓷炫的...
- d3d12dll丢失怎么修复?d3d12dll加载失败怎么解决?
-
d3d12.dll丢失怎么修复?d3d12.dll加载失败怎么解决?很多朋友想要运行游戏的时候都会遇到这个问题,这种情况该怎么办呢?今天系统之家小编给朋友们讲讲具体的解决方法,操作其实还蛮简单的。...
- 许多玩家反馈《生化4RE》PC一直崩溃 无法进入游戏
-
今日(3月24日),卡普空《生化危机4:重制版》正式发售,然而有部分PC玩家遇到了游戏崩溃等问题。很多玩家在贴吧发帖称游戏遇到了严重的崩溃问题,且经常反复,报错代码普遍为FatalD3Derror...
- 微软正式推出适用于WSL Linux的D3D12 GPU视频加速技术
-
今天,微软正式向WindowsSubsystemforLinux(WSL)用户发布了Direct3D12GPU视频加速支持。在微软通过WSL允许在Linux下使用Open...
- 《怪物猎人:崛起》曙光系统报错“Fatal d3d error”的解决办法
-
《怪物猎人:崛起》曙光系统报错“Fatald3derror”的解决办法不少小伙伴反应《怪物猎人:崛起》DLC曙光预载以后打不开游戏,出现了Fatald3derror类似的错误代码,这类问题的解...
- Mac+双屏,前端程序员的专业配置 - Loctek 乐歌 D3D 双屏电脑显示器支架
-
做FE也有一段日子了,电脑屏幕每天在设计稿、浏览器、IDE、即时通讯工具、Terminal、邮箱之间切换。虽然mac的工作区带来了很多灵活,但是依然略显不足。于是入手支架,把公司配的电脑和显示器发挥起...
- RPC 的原理和简单使用(rpc详解)
-
RPC的概念RPC,RemoteProcedureCall,翻译成中文就是远程过程调用,是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数。在调用的...
- 大厂开源的golang微服务rpc框架 — kitex
-
提前rpc估计所有的开发同学都知道,不知道的也无所谓,毕竟我也好几年没用了,今天带大家在复习一下。RPC(RemoteProcedureCall):远程过程调用,...
- 干货!一文掌握Protobuf所有语言所有用法,快收藏
-
说实话,Protobuf这个库,让人相见时难别亦难,东风无力百花残,每次等到要用它的时候,总感觉还没有完全掌握它的用法,而实际上等去百度或者谷歌的时候,教程都是多么的凌乱不堪。学会它,最直接关系到的,...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)