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的更多相关文章

  1. 【Java_多线程并发编程】JUC原子类——原子类中的volatile变量和CAS函数

    JUC中的原子类是依靠volatile变量和Unsafe类中的CAS函数实现的. 1. volatile变量的特性 内存可见性(当一个线程修改volatile变量的值后,另一个线程就可以实时看到此变量 ...

  2. 一起来看看java并发中volatile关键字的神奇之处

    并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volati ...

  3. Java并发编程:volatile关键字解析

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

  4. 【转】Java并发编程:volatile关键字解析

    转自:http://www.importnew.com/18126.html#comment-487304 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备 ...

  5. zz剖析为什么在多核多线程程序中要慎用volatile关键字?

    [摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...

  6. java并发编程(2)--volatile(转)

    转载:http://ifeve.com/volatile/ 作者:方 腾飞 花名清英,并发网(ifeve.com)创始人,畅销书<Java并发编程的艺术>作者,蚂蚁金服技术专家.目前工作于 ...

  7. 并发中的Native方法,CAS操作与ABA问题

    Native方法,Unsafe与CAS操作 >>JNI和Native方法 Java中,通过JNI(Java Native Interface,java本地接口)来实现本地化,访问操作系统底 ...

  8. (转)Java并发编程:volatile关键字解析

    转:http://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或 ...

  9. 《转》JAVA并发编程:volatile关键字解析

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

随机推荐

  1. day 06 字符串和列表的方法

    一.整形int 定义方式: age=18    #调用age=int(18)的方法,自动调用 n=int("123") #只能转换纯数字类型 二:浮点型float 定义方式 sal ...

  2. Python 进程池的同步方法

    import time from multiprocessing import Process,Pool def f1(n): time.sleep(1) #print(n) return n*n i ...

  3. webpack 3.X研究

    目前webpack已经到了3.X版本,为了方便通过最新版本打包整个前端应用,现在开始对webpack操作进行简单介绍. 一.webpack安装 在安装webpack之前需要注意,webpack是通过n ...

  4. 颜色的RGB-计算HSV公式色度/饱和度/亮度 简化代码

    计算颜色的HSV值 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享.心创新! ...

  5. jsp/servlet环境搭建

    手动配置servlet开发环境: 1. eclipse.tomcat.jdk下载安装: 2. eclipse新建项目,项目依赖tomcat的jar包(包含tomcat和servlet相关jar包)以及 ...

  6. 百度地图 JavaScript API

    最近有点懒  项目结尾了  完了好长时间 没有去总结项目中的问题 想了下还是写写吧 这是一个关于百度地图的 网页展示 <!DOCTYPE html><html><head ...

  7. myelipse与maven结合配置

    添加插件 指定maven中jar包默认仓库地址 maven中debug模式中断点关联不到项目代码的问题 解决方法参考文章:https://blog.csdn.net/urnot/article/det ...

  8. 重建二叉树(JAVA)

    重建二叉树 题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字. 例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历 ...

  9. spring redis 注解实现缓存机制

    1.xml配置 <bean id="poolConfigTax" class="redis.clients.jedis.JedisPoolConfig"& ...

  10. mvc项目用log4net 记录错误日志

    1.  首先下载lognet 下载地址 http://logging.apache.org/log4net/download_log4net.cgi 2.找到bin文件中的net文件夹  之后看你电脑 ...