并发中的volatile
1. 概述
由于线程有本地内存的存在, 一个线程修改的共享变量不会及时的刷新到主内存中, 使得另一个线程读取共享变量时读取到的仍旧是旧值, 就导致了内存可见性问题. 现在volatile就可以解决这个问题, 为什么能解决内存可见性问题呢? 本文就来揭开volatile的神秘面纱.
2. volatile的特性
理解volatile特性的一个好方法就是把对volatile单个变量的读/写, 看成是使用同一个锁对单个变量的读/写做了同步.
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性, 这意味着对一个volatile变量的读, 总是能看到(任意线程)对这个volatile变量最后的写入.
锁的语义决定了临界区代码的执行具有原子性. 这意味着, 即使是64位的long型和double型变量, 只要它是volatile变量, 对该变量的读/写就具有原子性. 如果是多个volatile操作或类似于volatile++这种复合操作, 这些操作整体上不具有原子性.
简言之, volatile变量自身具有以下特性.
- 可见性: 对一个volatile变量的读, 总是能看到任意线程对这个volatile变量最后的写入.
- 原子性: 对任意单个volatile变量的读/写具有原子性, volatile变量的复合操作不具有原子性.
3. volatile写-读的内存语义
volatile写的内存语义
当写一个volatile变量时, JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中.
volatile读的内存语义
当读一个volatile变量时, JMM会把线程对应的本地内存中的共享变量值置为无效, 线程接下来将从主内存中读取共享变量.
volatile内存语义总结
- 线程A写一个volatile变量, 实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息.
- 线程B读一个volatile变量, 实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息.
- 线程A写一个volatile变量, 随后线程B读这个volatile变量, 这个过程实质上是线程A通过主内存向线程B发送消息.
4. volatile内存语义的实现
前面提到过重排序分为编译器重排序和处理器重排序. 为了实现volatile语义, JMM会分别限制这两种类型的重排序类型.
JMM针对编译器制定的volatile重排序规则表

从图中可以看出:
- 当第二个操作是volatile写时, 不管第一个操作是什么, 都不能重排序. 这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后.
- 当第一个操作是volatile读时, 不管第二个操作是什么, 都不能重排序. 这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前.
- 当第一个操作是volatile写, 第二个操作是volatile读时, 不能重排序.
为了实现volatile的内存语义, 编译器在生成字节码时, 会在指令序列中插入内存屏障来禁止特定类型的处理器重排序. 对于编译器来说, 发现一个最优布置来最小化插入屏障的总数几乎不可能. 为此, JMM采取保守策略. 下面是基于保守策略的JMM内存屏障插入策略.
- 在每个volatile写操作的前面插入一个StoreStore屏障.
- 在每个volatile写操作的后面插入一个StoreLoad屏障.
- 在每个volatile读操作的后面插入一个LoadLoad屏障.
- 在每个volatile读操作的后面插入一个LoadStore屏障.
关于内存屏障可以看 https://www.cnblogs.com/wuqinglong/p/9947786.html
上述内存屏障插入策略非常保守, 但它可以保证在任意处理器平台, 任意的程序中都能得到正确的volatile内存语义.
在实际执行时, 只要不改变volatile写-读的内存语义, 编译器可以根据具体情况省略不必要的屏障. 举个例子.
有如下代码:
public class Demo {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // 第一个volatile读
int j = v2; // 第二个volatile读
a = i + j; // 普通写
v1 = i + 1; // 第一个volatile写
v2 = j * 2; // 第二个 volatile写
}
}
针对readAndWrite()方法, 理论上生成字节码时会如下:
int i = v1; // volatile读后面插入LoadLoad和LoadStore屏障
LoadLoad; // 确保v1的装载先于后续装载指令
LoadStore; // 确保v1的加载先于后续存储指令
int j = v2; // volatile读后面插入LoadLoad和LoadStore屏障
LoadLoad; // 确保v2的装载先于后续装载指令
LoadStore; // 确保v2的加载先于后续存储指令
a = i + j; // 普通读写无屏障
StoreStore; // 确保之前的存储指令要先于v1的存储
v1 = i + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令
StoreStore; // 确保之前的存储指令要先于v1的存储
v2 = j + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令
由于不同的处理器有不同的"松紧度"的处理器内存模型, 内存屏障的插入还可以根据具体的处理器内存模型继续优化, 以X86处理器为例, 处理最后的StoreLoad屏障外, 其它的屏障都会被省略. X86处理器仅会对写-读操作做重排序, 不会对读-读, 读-写和写-写操作做重排序. 因此X86处理器会省略掉这3中操作类型对应的内存屏障. 所以在X86处理器中, JMM仅需在volatile写后面插入一个StoreLoad屏障即可实现volatile写-读的内存语义.
下面是X86处理器优化之后的内存屏障
int i = v1; // volatile读后面插入LoadLoad和LoadStore屏障
// LoadLoad; // 确保v1的装载先于后续装载指令
// LoadStore; // 确保v1的加载先于后续存储指令
int j = v2; // volatile读后面插入LoadLoad和LoadStore屏障
// LoadLoad; // 确保v2的装载先于后续装载指令
// LoadStore; // 确保v2的加载先于后续存储指令
a = i + j; // 普通读写无屏障
// StoreStore; // 确保之前的存储指令要先于v1的存储
v1 = i + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令
// StoreStore; // 确保之前的存储指令要先于v1的存储
v2 = j + 1; // volatile写前加StoreStore屏障, 后加StoreLoad屏障
StoreLoad; // 确保v1的存储要先于后续的装载指令
5. JSR-133为什么要增强volatile的内存语义
JSR-133也就是在JDK1.5中加入的.
在JSR-133之前的旧Java内存模型中, 虽然不允许volatile变量之间重排序, 但旧的Java内存模型允许volatile变量与普通变量重排序.

在旧的内存模型中, 当1和2之间没有数据依赖关系时, 1和2之间就可能被重排序(3和4类似). 其结果就是: 读线程B执行4时, 不一定能看到写线程A在执行1时对共享变量的修改.
因此, 在旧的内存模型中, volatile的写-读没有锁的释放-获所具有的内存语义. 为了提供一种比锁更轻量级的线程之间通信的机制, JSR-133专家组决定增强volatile的内存语义: 严格限制编译器和处理器对volatile变量与普通变量的重排序, 确保volatile的写-读和锁的释放-获取具有相同的内存语义. 从编译器重排序规则和处理器内存屏障插入策略来看, 只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义, 这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止.
6. 总结
volatile能保证内存可见性正是通过内存屏障来实现的, 并且不同的编译器对内存屏障的支持不同, 但是由于大多数处理器都使用了写缓冲区, 所以大多数处理器都支持StoreLoad屏障.
并发中的volatile的更多相关文章
- 【Java_多线程并发编程】JUC原子类——原子类中的volatile变量和CAS函数
JUC中的原子类是依靠volatile变量和Unsafe类中的CAS函数实现的. 1. volatile变量的特性 内存可见性(当一个线程修改volatile变量的值后,另一个线程就可以实时看到此变量 ...
- 一起来看看java并发中volatile关键字的神奇之处
并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volati ...
- Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- 【转】Java并发编程:volatile关键字解析
转自:http://www.importnew.com/18126.html#comment-487304 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备 ...
- zz剖析为什么在多核多线程程序中要慎用volatile关键字?
[摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...
- java并发编程(2)--volatile(转)
转载:http://ifeve.com/volatile/ 作者:方 腾飞 花名清英,并发网(ifeve.com)创始人,畅销书<Java并发编程的艺术>作者,蚂蚁金服技术专家.目前工作于 ...
- 并发中的Native方法,CAS操作与ABA问题
Native方法,Unsafe与CAS操作 >>JNI和Native方法 Java中,通过JNI(Java Native Interface,java本地接口)来实现本地化,访问操作系统底 ...
- (转)Java并发编程:volatile关键字解析
转:http://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或 ...
- 《转》JAVA并发编程:volatile关键字解析
volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...
随机推荐
- L2-016. 愿天下有情人都是失散多年的兄妹(深搜)*
L2-016. 愿天下有情人都是失散多年的兄妹 参考博客 #include<iostream> #include<cstdio> #include<cstring> ...
- Arch Linux 的休眠设置
https://wiki.archlinux.org/index.php/Power_management/Suspend_and_hibernate_(简体中文)https://wiki.archl ...
- 第三节《Git重置》
先来看看.git/refs/heads/master文件的内容 [root@git demo]# cat .git/refs/heads/master e97f443b2d1cee7eeca7dc2e ...
- 第十七章 java8特性
17.java8中Lambda表达式与Stream API的使用 17.1 Lambda 表达式(Lambda Expressions) 1课时 17.2 函数式(Functional)接口 1课时 ...
- 第二章 JavaScript案例(中)
1. js事件 HTML代码 <!DOCTYPE html> <html lang="en" onUnload="ud()"> < ...
- zabbix使用微信报警(四)
https://qy.weixin.qq.com/ 企业号注册 http://qydev.weixin.qq.com/wiki/index.php?title=%E9%A6%96%E9%A1%B5 ...
- Docker常用命令(四)
通过一些例子来了解基本的命令使用 1.查看docker信息 docker info 2.安装完Docker后,里面还有任何镜像,先从仓库下载一个基础镜像,然后在这个基础 ...
- ant design + react,自动获取上传音频的时长(react-audio-player)
在后台管理项目中,用户要求上传音频,并且自动获取音频时长. 第一步, import { Upload, Button, Icon } from 'antd'; 第二步,在表单中使用 Upload 组件 ...
- super超类继承特点小结
super超类继承特点小结: 1. super并不是一个函数,是一个类名,形如super(B, self)事实上调用了super类的初始化函数,产生了一个super对象: 2. super类的初始化函 ...
- Azure CosmosDB (2) CosmosDB中的数据一致性
<Windows Azure Platform 系列文章目录> 为了保证分布式数据库的高可用性和低延迟性,我们需要在可用性.延迟和吞吐量之间进行权衡. 绝大部分的商业分布式数据库,要求开发 ...