深入理解 volatile
volatile 是轻量级的synchronized,它在多处理开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile 定义与实现原理
volatile的定义:Java编程语言允许线程访问共享变量,为了保证共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量
如下展示使用hsdis查看jit生成的汇编代码过程,通过汇编指令来查看volatile的语义:
//源代码
public class VolatileTest {volatile static StringBuilder str = new StringBuilder("Hello world!");public static void main(String[] args) {str.append("My Lover!");System.out.println(str);}}
- 下载hsdis工具,将其放在 %JAVA_HOME%\jre\bin\server 目录下
- 使用如下命令得到汇编代码
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly//了解内联优化的实际情况以及优化发生的级别:
java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:+TieredCompilation VolatileTest
部分汇编代码如下:
0x000000000328326a: or %r15,%rdi0x000000000328326d: lock cmpxchg %rdi,(%rdx)0x0000000003283272: jne 0x00000000032836c50x0000000003283278: jmpq 0x00000000032832b80x000000000328327d: mov 0x8(%rdx),%edi0x0000000003283280: shl $0x3,%rdi0x0000000003283284: mov 0xa8(%rdi),%rdi0x000000000328328b: lock cmpxchg %rdi,(%rdx)0x0000000003283290: mov (%rdx),%rax0x0000000003283293: or $0x1,%rax0x0000000003283297: mov %rax,(%rsi)0x000000000328329a: lock cmpxchg %rsi,(%rdx) //volatile修饰的变量前加上lock前缀0x000000000328329f: je 0x00000000032832b80x00000000032832a5: sub %rsp,%rax0x00000000032832a8: and $0xfffffffffffff007,%rax0x00000000032832af: mov %rax,(%rsi)0x00000000032832b2: jne 0x00000000032836c50x00000000032832b8: movabs $0x16c811e0,%rsi ; {metadata(method data for {method} {0x0000000016a9ff08} 'append' '(C)Ljava/lang/StringBuffer;' in 'java/lang/StringBuffer')}0x00000000032832c2: mov 0xdc(%rsi),%edi0x00000000032832c8: add $0x8,%edi0x00000000032832cb: mov %edi,0xdc(%rsi)0x00000000032832d1: movabs $0x16a9ff00,%rsi ; {metadata({method} {0x0000000016a9ff08} 'append' '(C)Ljava/lang/StringBuffer;' in 'java/lang/StringBuffer')}0x00000000032832db: and $0x1ff8,%edi0x00000000032832e1: cmp $0x0,%edi0x00000000032832e4: je 0x00000000032836d8 ;*aload_0; - java.lang.StringBuffer::append@0 (line 380)
volatile的原理:volatile变量修饰的共享变量进行写操作时会在汇编代码前加上lock前缀,lock前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 该写回内存的操作会使在其他CPU里缓存了该内存地址的额数据无效
volatile 的特性
把对volatile变量的单个 读/写 看成是使用同一个锁对这些单个 读/写 操作做了同步,如下是使用 volatile变量 和 使用 方法锁(synchronized) 实现等同的语义:
class VolatileFeaturesExample {volatile long vl = 0L; // 使用volatile声明64位的long型变量public void set(long l) {vl = l; // 单个volatile变量的写(具有原子性)} public void getAndIncrement () {vl++; // 复合(多个)volatile变量的读/写(不具有原子性)} public long get() {return vl; // 单个volatile变量的读}
}class VolatileFeaturesExample {long vl = 0L; // 64位的long型普通变量public synchronized void set(long l) { // 对单个的普通变量的写用同一个锁同步vl = l;} public void getAndIncrement () { // 普通方法调用long temp = get(); // 调用已同步的读方法temp += 1L; // 普通写操作set(temp); // 调用已同步的写方法} public synchronized long get() { // 对单个的普通变量的读用同一个锁同步return vl;}
}
锁的 happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。锁的语意决定了临界区代码的执行具有原子性,这意味着volatile修饰的64位long型变量和double变量,在对其进行 读/写 时具有原子性。如果是多个 volatile操作(类似于 volatile++这种复合操作)就不具有原子性。volatile变量自身具有下列特性:
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的 读/写 具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile建立的 happens-before
从JDK1.5开始,volatile变量的 写-读 可以实现线程间的通信。从内存语意上来讲,volatile的 写-读 与锁的 释放-获取 有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取具有相同的内存语义。
//假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {int a = 0;volatile boolean flag = false;public void writer() {a = 1; // 1 线程A修改共享变量flag = true; // 2 线程A写volatile变量} public void reader() {if (flag) { // 3 线程B读同一个volatile变量int i = a; // 4 线程B读共享变量……}}
}
根据happens-before规则,如上过程建立了3类happens-before关系:
- 根据程序次序规则:1 happens-before 2;3 happens-before 4。
- 根据volatile规则:2 happens-before 3。
- 根据happens-before的传递性规则:1 happens-before 4。
A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。
volatile的内存语义
- volatile写的内存语义:当写一个volatile变量时,JMM(Java内存模型)会把该线程对应的本地内存中的共享变量值刷新到主内存。
- volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
volatile写和volatile读的内存语义总结:
线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了消息(对共享变量所做的修改)——通信。
线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的消息(volatile写)。
线程A写一个volatile变量,线程B读一个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
volatile内存语义的实现
为了实现volatile内存语义,JMM会分别限制两种类型的重排序:编译重排序和处理器重排序。
如下JMM针对编译器制定的volatile重排序规则:
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
基于保守策略的JMM内存屏障插入策略(为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序) :
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
保守策略下volatile写插入内存屏障后生成指令的示意图:
保守策略下volatile读插入内存屏障后生成指令的示意图:
volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。
上述volatile写和volatile读的内存屏障非常保守。在实际执行时,只要不改变volatile 写-读 的内存语义,编译器可以根据具体情况省略不必要的内存屏障,如下例子:
class VolatileBarrierExample {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写} …// 其他方法
}
编译器在生成字节码时做出如下优化:
注意:最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器通常会在这里插入一个StoreLoad屏障。
volatile变量的使用
加锁机制可以确保可见性和原子性,volatile变量只能确保可见性。volatile变量通常用做某个操作完成,发生中断或者状态的标志。
仅当volatile变量能简化代码的实现以及对同步策略的验证时才应该被使用。如果在验证正确性时需要对可见性进行复杂的判断,就不应该使用volatile变量。volatile变量的正确使用方式包括:确保自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期时间的发生(eg:初始化或关闭)
当且仅当满足以下所有条件时,才应该使用volatile变量:
对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值
该变量不会与其他状态变量一起纳入不变性条件之中
在访问变量时不需要加锁
深入理解 volatile相关推荐
- 深入理解volatile
在理解volatile之前,我们先来看下CPU的工作模式: 处理器这种工作产生的问题: 1.所有的变量在处理器运算期间都是变量对应值的一个副本,其它处理器无法感知其对变量的操作. 2.处理器为了高效利 ...
- 从缓存行出发理解volatile变量、伪共享False sharing、disruptor
volatile关键字 当变量被某个线程A修改值之后,其它线程比如B若读取此变量的话,立刻可以看到原来线程A修改后的值 注:普通变量与volatile变量的区别是volatile的特殊规则保证了新值能 ...
- 对精致码农大佬的 [理解 volatile 关键字] 文章结论的思考和寻找真相
一:背景 1. 讲故事 昨天在园里的编辑头条看到 精致码农大佬 写的一篇题为:[C#.NET 拾遗补漏]10:理解 volatile 关键字 (https://www.cnblogs.com/will ...
- Java 多线程 —— 深入理解 volatile 的原理以及应用
转载自 Java 多线程 -- 深入理解 volatile 的原理以及应用 推荐阅读:<java 多线程-线程怎么来的> 这一篇主要讲解一下volatile的原理以及应用,想必看完这一篇 ...
- 【Java线程】深入理解Volatile关键字和使用
目录 背景 volatile原理 volatile特性 可见性 有序性 原子性 使用场景 背景 理解volatile底层原理之前,首先介绍关于缓存一致性协议的知识. 背景:计算机在执行程序时,每条指令 ...
- [C#.NET 拾遗补漏]10:理解 volatile 关键字
要理解 C# 中的 volatile 关键字,就要先知道编译器背后的一个基本优化原理.比如对于下面这段代码: public class Example {public int x;public voi ...
- 多线程 转账_多线程编程不可错过——彻底理解volatile
持续分享互联网一线研发技术,欢迎关注我. volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Ja ...
- 深入理解volatile关键字---缓存一致性原理
volatile关键字与缓存一致性 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java ...
- 深入理解volatile(Java)
前言 除了上篇文章讲到的关键字synchronize关键字外可以实现同步外,java中还有另一个关键字volatile可以实现一些简单的同步. synchronized知识的了解可查看深入理解sync ...
- auto.js停止所有线程_Java线程与并发编程实践:深入理解volatile和final变量
同步有两种属性:互斥性和可见性.synchronized关键字与两者都有关系.Java同时也提供了一种更弱的.仅仅包含可见性的同步形式,并且只以volatile关键字关联. 假设你自己设计了一个停止线 ...
最新文章
- 如何让敏捷中的每日站会发挥最大效果?
- CVPR 2018 | 腾讯AI Lab入选21篇论文详解
- mysql 单号,利用 MySQL 自增列生成订单号
- Eclipse的Git插件下载项目出现 No project found
- python time模块详解
- 舞台现场直播技术实践
- 百会CRM教你在大数据平台中做精准营销
- Python Web初学解惑之 WSGI、flup、fastcgi、web.py的关系
- springcloud阿里巴巴五大组件_如何无缝迁移 SpringCloud/Dubbo 应用到 Serverless 架构
- c语言第四阶段在线作业,中石大(华东)12春(41400) 《C语言》第四阶段在线作业(自测)...
- python or 和and的优先级_python中not、and和or的优先级与详细用法介绍
- 电子设计(2)三极管稳压电路仿真分析
- 如何将pdf分割成一页一页的
- nacos启动后CPU使用率过高
- Golang 操作临时文件和目录
- 热烈欢迎中国中小商业企业协会叶焙副会长、沈亚桂秘书长一行莅临萝卜科技
- 黑客攻击第二次,开redis一瞬间
- pair用法(给元素赋值)
- 深度学习模型DNN部署到安卓设备上全流程示例——{pytorch->onnx>ncnn->Android studio}
- 互联网 Java 工程师进阶知识完全扫盲
热门文章
- 中国式父母计算机科学家攻略,中国式家长怎么满分_中国式家长第一代满分攻略_3DM单机...
- android9 人脸解锁,小米MIUI 9上线后新增功能盘点:人脸解锁最实用
- 图形芯片RSX拖后腿!PS3如期发售困难重重
- C盘满了怎么分析C盘使用情况 找出C盘大文件
- 浙江工商大学813理学统计学复试常见问题整理总结——数理统计部分补充(方差分析与回归分析)
- 算法分析与设计 二分查找
- AP资产 | 量化多岗位招聘(全职+实习)
- 通达信 移动平均算法_单片机数字滤波的算法
- 【前端学习笔记day35】4.5. Photoshop制作雪碧图技巧
- ipad air2 拆机如何拔出电池?