一 引言

  听说在Java 5之前volatile关键字备受争议,所以本文也不讨论1.5版本之前的volatile。本文主要针对1.5后即JSR-133针对volatile做了强化后的了解。

二 volatile的特性

  开门见山,volatile变量自身具有以下特性:

  • 可见性(最重要的特性)。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性。对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性。但是类型于a++这种复合操作不具有原子性。

  下面通过案例来证明下可见性,先看一个普通变量是否能保证可见性:

 3 class Example {
4 private boolean stop = false;
5 public void execute() {
6 int i = 0;
7 System.out.println("thread1 start loop.");
8 while(!getStop()) {
9 i++;
10 }
11 System.out.println("thread1 finish loop,i=" + i);
12 }
13 public boolean getStop() {
14 return stop; // 对普通变量的读
15 }
16 public void setStop(boolean flag) {
17 this.stop = flag; // 对普通变量的写
18 }
19 }
20 public class VolatileExample {
21 public static void main(String[] args) throws Exception {
22 final Example example = new Example();
23 Thread t1 = new Thread(new Runnable() {
24 @Override
25 public void run() {
26 example.execute();
27 }
28 });
29 t1.start();
30
31 Thread.sleep(1000);
32 System.out.println("主线程即将置stop值为true...");
33 example.setStop(true);
34 System.out.println("主线程已将stop值为:" + example.getStop());
35 System.out.println("主线程等待线程1执行完...");
36
37 t1.join();
38 System.out.println("线程1已执行完毕,整个流程结束...");
39 }
40 }

  上面程序的意思是:让线程1先执行然后主(main)线程修改标志看是否能让子线程跳出循环。执行程序后发现程序并没有执行完,而是在等待线程1执行完毕。这就说明主线程修改stop变量并不对线程1可见,所以普通变量是不保证可见性的。

  当你把变量stop用volatile修饰时,主线程修改stop变量会立马对线程1可见并终止程序,这就证明volatile变量是具有可见性特性的。下面修改后的结果。

  原子性特性已经说的很清楚了(对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性),记着是对单个volatile变量的读或写才具有原子性(如果要进行测试的话,将上面案例的volatile变量修改成long/double类型,测试逻辑一样,只不过将它放在x86的机器上运行。因为在x86的机器上不能保证long类型和double类型的原子性的,具体原因在Java内存模型中的顺序一致性一节有说明)。另外任何复合操作都不能保证原子性,如a++,a = a+1, a = b。特别注意a = b这类,它实际上包含2个操作,它先要去读取b的值,再将b的值写入工作内存,虽然读取b的值以及将b的值写入工作内存这2个操作都是原子性操作,但是合起来就不是原子性操作了。

  想要理解透volatile特性有一个很好的方法,就是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。

三 volatile写-读建立的happens-before关系

  这个详细在happens-before规则中说明。

四 volatile写-读的内存语义

  • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

  • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

  以上面VolatileExample程序为例进行简单说明,当主线程对stop进行修改后且子线程尚未对stop进行读时,主线程已经把stop的值刷新到了主内存。其示意图如下:

  当子线程进行读取时,会把本地内存置为无效直接去主内存中读取。(这里的主线程和子线程可以了解为两个普通线程没有父子关系)其示意图如下:

五 volatile内存语义的实现

  为了实现volatile的内存语义,JMM会分别限制这两种类型的重排序。下图是JMM针对编译器指定的volatile重排序规则表。

  • 当第二个操作为volatile写操作时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile写之后;
  • 当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile读之前;
  • 当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。

  为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障(在JMM也有提到过且有说明对应的几种屏障的作用,请仔细阅读)来禁止特定类型的处理器重排序。下面是基于保守策略的 JMM 内存屏障插入策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。

  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)。

  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操作重排序)。

  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操作重排序)。

  其中重点说下StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。上述内存屏障插入策略非常保守,但它可以保证在任意处理平台,任意的程序中都能得到正确的volatile语义。下面是保守策略(为什么说保守呢,因为有些在实际的场景是可省略的)下,volatile 写操作 插入内存屏障后生成的指令序列示意图:

  其中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作对任意处理器可见(把它刷新到主内存)。另外volatile写后面有StoreLoad屏障,此屏障的作用是避免volatile写与后面可能有的读或写操作进行重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)为了保证能正确实现volatile的内存语义,JMM采取了保守策略:在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见模式是:一个写线程写volatile变量,多个度线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里也可看出JMM在实现上的一个特点:首先确保正确性,然后再去追求效率(其实我们工作中编码也是一样)。

  下面是在保守策略下,volatile读插入内存屏障后生产的指令序列示意图:

  上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况忽略不必要的屏障。在JMM基础中就有提到过各个处理器对各个屏障的支持度,其中x86处理器仅会对写-读操作做重排序。

六 总结

  volatile主要作用是具有可见性和原子性(单个变量),其实现原理就是利用屏障来保障实现。要想彻底掌握就应该多做下相关场景的编码,经典的场景有:状态标记量、volatile方式的double check等。

  以上如有错误之处,欢迎指出,欢迎讨论,谢谢!

  

Java内存模型-volatile的内存语义的更多相关文章

  1. 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

    前言 楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中.在一次学习中去理解这些东西.才能更好的理解 Jav ...

  2. java内存模型-volatile

    volatile 的特性 当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别.理解 volatile 特性的一个好方法是:把对 volatile 变量的单个读/写,看成是使用同一 ...

  3. 深入理解Java内存模型 - volatile

    volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这 ...

  4. 深入理解Java内存模型——volatile

    volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会非常特别. 理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁 ...

  5. 【java多线程系列】java中的volatile的内存语义

    在java的多线程编程中,synchronized和volatile都扮演着重要的 角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性,可见性指的是当一 ...

  6. JAVA锁和volatile的内存语义&volatile的使用场景

    JAVA锁的内存语义 当线程释放锁时,JMM(Java Memory Model)会把该线程对应的本地内存中的共享变量刷新到主内存中. 当线程获取锁时,JMM会将该线程对应的本地内存置为无效.从而使得 ...

  7. Java并发编程里的volatile。Java内存模型核CPU内存架构的对应关系

    CPU内存架构:https://www.jianshu.com/p/3d1eb589b48e Java内存模型:https://www.jianshu.com/p/27a9003c33f4 多线程下的 ...

  8. Java内存模型-锁的内存语义

    一 引言 在说volatile的内存语义时,讲过这样一句话:想要理解透volatile特性有一个很好的方法,就是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.所 ...

  9. JMM内存模型+volatile+synchronized+lock

    硬件内存模型: Java内存模型: 每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个内存共享. 下面 8 个操作都是原子的,不可再分的: 1)  lock ...

随机推荐

  1. PAT 甲级 1021 Deepest Root

    https://pintia.cn/problem-sets/994805342720868352/problems/994805482919673856 A graph which is conne ...

  2. 对象函数的readFileSyc类

    对于所有的Syc后缀都是表示同步,默认不加是异步操作.

  3. CMS垃圾收集器与G1收集器

    1.CMS收集器 CMS收集器是一种以获取最短回收停顿时间为目标的收集器.基于“标记-清除”算法实现,它的运作过程如下: 1)初始标记 2)并发标记 3)重新标记 4)并发清除 初始标记.从新标记这两 ...

  4. jar 命令详解

    jar 是随 JDK 安装的,在 JDK 安装目录下的 bin 目录中,Windows 下文件名为 jar.exe,Linux 下文件名为 jar.它的运行需要用到 JDK 安装目录下 lib 目录中 ...

  5. Linux命令(二十三) 磁盘管理命令(一) df,du,tune2fs

    一. 查看磁盘占用空间情况 df df 命令用于查看硬盘空间的使用情况,还可以查看硬盘分区的类型或 inode 节点的使用情况等. df 命令常用参数如下: -a 显示所有文件系统的磁盘使用情况,包括 ...

  6. [转帖]Application Request Route实现IIS Server Farms集群负载详解

    Application Request Route实现IIS Server Farms集群负载详解  https://www.cnblogs.com/knowledgesea/p/5099893.ht ...

  7. OneZero第二周第二次站立会议(2016.3.29)

    会议时间:2016年3月29日  13:05~13:16 会议成员:冉华,张敏,王巍,夏一鸣. 会议目的:汇报前一天工作,全体成员评论,确定会后修改内容或分配下一步任务. 会议内容:以下是会议插图 1 ...

  8. pgm7

    和 Koller 的 video 最大的不同莫过于书上讲 LBP 的角度不是 procedural 的,而是原理性的.我们先看个 procedural 的,在一般的 cluster graph 上的 ...

  9. 【BZOJ1044】[HAOI2008]木棍分割(动态规划,贪心)

    [BZOJ1044][HAOI2008]木棍分割(动态规划,贪心) 题面 BZOJ 洛谷 题解 第一问随便二分一下就好了,贪心\(check\)正确性显然. 第二问随便前缀和+单调队列优化一下\(dp ...

  10. unity3d 几种镜头畸变

    1.Fisheye distortion  鱼眼镜头 解释来自百度百科:鱼眼镜头是一种焦距为16mm或更短的并且视角接近或等于180°. 它是一种极端的广角镜头,“鱼眼镜头”是它的俗称.为使镜头达到最 ...