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. WPF Blend Grid 布局

    这几天都在用blend拖拽界面.我想要的效果是 放大后出现的效果是 但实际出来的效果是放大以后能看到所有的控件,缩小以后窗体就把控件个遮住了.怎么办? 在WPF中提供了9种布局方式,具体Grid,Ca ...

  2. Python全栈之路----类型转换

    显式转换 int(x [,base]) ⇒ 将x转换为一个十进制的整数 long(x [,base]) ⇒ 将x转换为一个十进制的长整数 float(x) ⇒ 将x转换为一个浮点数 str(objec ...

  3. CCF-URL映射-(正则匹配)-20180303

    果然正则表达式是一个强大的工具 更短的代码....hhh 版本1: 正则表达式..so easy~~ #include <iostream> #include <algorithm& ...

  4. Eclipse 护眼背景色设置

    链接地址:http://blog.chinaunix.net/uid-27183448-id-3509010.html 背景颜色推荐:色调:85,饱和度:123,亮度:205  文档都不再是刺眼的白底 ...

  5. 《从Lucene到Elasticsearch:全文检索实战》学习笔记五

    今天我给大家讲讲tf-idf权重计算 tf-idf权重计算: tf-idf(中文词频-逆文档概率)是表示计算词项对于一个文档集或语料库中的一份文件的重要程度.词项的重要性随着它在文档中出现的次数成正比 ...

  6. Java分布式锁

    分布式锁简述 在单机时代,虽然不存在分布式锁,但也会面临资源互斥的情况,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就 ...

  7. html笔记汇总

    <html lang="en-us"> HTML标签的lang属性(language)指定当前页面所使用的自然语言,如zh.zh-cn.zh-tw.zh-hk.en.e ...

  8. C# 数据库

    连接: using System; using System.Collections.Generic; using System.Linq; using System.Text; using Syst ...

  9. Docker入门与实战讲解

    转载自:http://blog.csdn.net/relax_hb/article/details/69668815 简述 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包 ...

  10. java中的内部类详解

    https://www.cnblogs.com/dolphin0520/p/3811445.html https://www.cnblogs.com/chenssy/p/3388487.html