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相关推荐

  1. 深入理解volatile

    在理解volatile之前,我们先来看下CPU的工作模式: 处理器这种工作产生的问题: 1.所有的变量在处理器运算期间都是变量对应值的一个副本,其它处理器无法感知其对变量的操作. 2.处理器为了高效利 ...

  2. 从缓存行出发理解volatile变量、伪共享False sharing、disruptor

    volatile关键字 当变量被某个线程A修改值之后,其它线程比如B若读取此变量的话,立刻可以看到原来线程A修改后的值 注:普通变量与volatile变量的区别是volatile的特殊规则保证了新值能 ...

  3. 对精致码农大佬的 [理解 volatile 关键字] 文章结论的思考和寻找真相

    一:背景 1. 讲故事 昨天在园里的编辑头条看到 精致码农大佬 写的一篇题为:[C#.NET 拾遗补漏]10:理解 volatile 关键字 (https://www.cnblogs.com/will ...

  4. Java 多线程 —— 深入理解 volatile 的原理以及应用

    转载自  Java 多线程 -- 深入理解 volatile 的原理以及应用 推荐阅读:<java 多线程-线程怎么来的> 这一篇主要讲解一下volatile的原理以及应用,想必看完这一篇 ...

  5. 【Java线程】深入理解Volatile关键字和使用

    目录 背景 volatile原理 volatile特性 可见性 有序性 原子性 使用场景 背景 理解volatile底层原理之前,首先介绍关于缓存一致性协议的知识. 背景:计算机在执行程序时,每条指令 ...

  6. [C#.NET 拾遗补漏]10:理解 volatile 关键字

    要理解 C# 中的 volatile 关键字,就要先知道编译器背后的一个基本优化原理.比如对于下面这段代码: public class Example {public int x;public voi ...

  7. 多线程 转账_多线程编程不可错过——彻底理解volatile

    持续分享互联网一线研发技术,欢迎关注我. volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Ja ...

  8. 深入理解volatile关键字---缓存一致性原理

    volatile关键字与缓存一致性 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java ...

  9. 深入理解volatile(Java)

    前言 除了上篇文章讲到的关键字synchronize关键字外可以实现同步外,java中还有另一个关键字volatile可以实现一些简单的同步. synchronized知识的了解可查看深入理解sync ...

  10. auto.js停止所有线程_Java线程与并发编程实践:深入理解volatile和final变量

    同步有两种属性:互斥性和可见性.synchronized关键字与两者都有关系.Java同时也提供了一种更弱的.仅仅包含可见性的同步形式,并且只以volatile关键字关联. 假设你自己设计了一个停止线 ...

最新文章

  1. 如何让敏捷中的每日站会发挥最大效果?
  2. CVPR 2018 | 腾讯AI Lab入选21篇论文详解
  3. mysql 单号,利用 MySQL 自增列生成订单号
  4. Eclipse的Git插件下载项目出现 No project found
  5. python time模块详解
  6. 舞台现场直播技术实践
  7. 百会CRM教你在大数据平台中做精准营销
  8. Python Web初学解惑之 WSGI、flup、fastcgi、web.py的关系
  9. springcloud阿里巴巴五大组件_如何无缝迁移 SpringCloud/Dubbo 应用到 Serverless 架构
  10. c语言第四阶段在线作业,中石大(华东)12春(41400) 《C语言》第四阶段在线作业(自测)...
  11. python or 和and的优先级_python中not、and和or的优先级与详细用法介绍
  12. 电子设计(2)三极管稳压电路仿真分析
  13. 如何将pdf分割成一页一页的
  14. nacos启动后CPU使用率过高
  15. Golang 操作临时文件和目录
  16. 热烈欢迎中国中小商业企业协会叶焙副会长、沈亚桂秘书长一行莅临萝卜科技
  17. 黑客攻击第二次,开redis一瞬间
  18. pair用法(给元素赋值)
  19. 深度学习模型DNN部署到安卓设备上全流程示例——{pytorch->onnx>ncnn->Android studio}
  20. 互联网 Java 工程师进阶知识完全扫盲

热门文章

  1. 中国式父母计算机科学家攻略,中国式家长怎么满分_中国式家长第一代满分攻略_3DM单机...
  2. android9 人脸解锁,小米MIUI 9上线后新增功能盘点:人脸解锁最实用
  3. 图形芯片RSX拖后腿!PS3如期发售困难重重
  4. C盘满了怎么分析C盘使用情况 找出C盘大文件
  5. 浙江工商大学813理学统计学复试常见问题整理总结——数理统计部分补充(方差分析与回归分析)
  6. 算法分析与设计 二分查找
  7. AP资产 | 量化多岗位招聘(全职+实习)
  8. 通达信 移动平均算法_单片机数字滤波的算法
  9. 【前端学习笔记day35】4.5. Photoshop制作雪碧图技巧
  10. ipad air2 拆机如何拔出电池?