volatile详解
liebian365 2025-01-04 21:17 197 浏览 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 |
相关推荐
- C#夯实基础-Lambda在List中的使用
-
在C#中基本类型比如List,Dictionary,数组等都有委托来实现相关的操作。此时Lambda表达式就可以使用了.实例1,查找字符串List的包含a的元素...
- 在C#中,如何实现对集合中元素的自定义排序?
-
在C#中,可以通过多种方式实现对集合中元素的自定义排序,主要包括:...
- C++11 新特性面试题_c++ 11 面试题
-
1、C++11中引入了哪些新的智能指针类型?请描述它们的用法和区别。C++11中引入了三种新的智能指针类型:std::unique_ptr,std::shared_ptr,和std::weak_...
- 为什么要使用lambda表达式?原来如此,涨知识了
-
为什么要使用Lambda表达式先看几段Java8以前经常会遇到的代码:创建线程并启动...
- [编程基础] Python lambda函数总结
-
Pythonlambda函数教程展示了如何在Python中创建匿名函数。Python中的匿名函数是使用lambda关键字创建的。...
- 硬核!Java 程序员必须掌握的 10 个 简化代码的 Lambda 表达式!
-
大家好,我是一位在架构师道路上狂奔的码农,今天给大家介绍一下程序员必须掌握的10个Lambda表达式,这些表达式几乎涵盖了在实际编程中经常用到的常见场景。相信通过这10个Lambda表...
- 一文读懂lambda表达式_lambda表达式由来
-
作者:youngyan,腾讯PCG数据工程工程师...
- Java基础知识 - lambda 表达式_javalambda表达式用法
-
1、表达式语法1)lambda的命名采用的是数学符号λ;...
- Python学习笔记 | 匿名函数lambda、映射函数map和过滤函数filter
-
什么是匿名函数?定义:没有函数名的自定义函数场景:函数体非常简单,使用次数很少,没有必要声明函数,通常搭配高阶函数使用。...
- Java Lambda表达式详解(非常全面)
-
JavaLambda表达式是JDK8引入的,是一个比较重要的特性。@mikechenLambda表达式简介...
- 了解 Lambda:Python 中的单个表达式函数
-
Python中的lambda关键字提供了声明小型匿名函数的快捷方式。Lambda函数的行为与使用...
- 在C#中使用Lambda编写一个排序算法,比较其与传统排序算法的优劣
-
使用Lambda表达式编写排序算法在C#中,Lambda表达式可以用来简化排序逻辑的编写,尤其是在需要自定义排序规则时非常方便。以下示例展示了如何用Lambda表达式实现排序,并与传统排...
- 一日一技:python中的匿名函数 lambda用法
-
匿名函数lambda,语法如下:lambdaarguments:expression...
- 《回炉重造》——Lambda表达式_回炉重造是贬义词吗
-
前言Lambda表达式(LambdaExpression),相信大家对Lambda肯定是很熟悉的,毕竟我们数学上经常用到它,即λ。不过,感觉数学中的Lambda和编程语言中的Lamb...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 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)