一 引言

  听说在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. 剑指offer:包含min函数的栈

    题目描述: 定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1)). 解题思路: 相当与在保留原栈的同时,去维护一个最小栈.利用一个辅助栈来完成.对于每个 ...

  2. java向上转型和向下转型

    转型是在继承的基础上而言的,继承是面向对象语言中,代码复用的一种机制,通过继承,子类可以复用父类的功能,如果父类不能满足当前子类的需求,则子类可以重写父类中的方法来加以扩展. 向上转型:子类引用的对象 ...

  3. A总结

    Alpha 答辩总结 评审表 组名 格式 内容 ppt 演讲 答辩 总计 天机组 15 15 14 15 14 73 PMS 16 16 15 15 16 78 日不落战队 16 16 16 15 1 ...

  4. 从苦逼到牛逼,详解Linux运维工程师的打怪升级之路

    做运维也快四年多了,就像游戏打怪升级,升级后知识体系和运维体系也相对变化挺大,学习了很多新的知识点. 运维工程师是从一个呆逼进化为苦逼再成长为牛逼的过程,前提在于你要能忍能干能拼,还要具有敏锐的嗅觉感 ...

  5. Redis的相关问题总结

    一.Redis的优缺点及适用场景 Redis 是一个基于内存的高性能key-value数据库.很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘 ...

  6. Java并发编程之深入理解线程池原理及实现

    Java线程池在实际的应用开发中十分广泛.虽然Java1.5之后在JUC包中提供了内置线程池可以拿来就用,但是这之前仍有许多老的应用和系统是需要程序员自己开发的.因此,基于线程池的需求背景.技术要求了 ...

  7. Codeforces Round #485 Div. 1 vp记

    A:对每种商品多源bfs一下每个点到该商品的最近距离,对每个点sort一下取前s个即可. #include<iostream> #include<cstdio> #includ ...

  8. FortiGate 硬件加速

    FortiGate 硬件加速 来源 https://wenku.baidu.com/view/07749195a1c7aa00b52acb63.html 硬件加速 来源 https://blog.cs ...

  9. pandas read_csv 读取中文列标题文件报错

    Traceback (most recent call last): File "C:/Users/arron/PycharmProjects/ML/ML/test.py", li ...

  10. BZOJ 3876 支线剧情 | 有下界费用流

    BZOJ 3876 支线剧情 | 有下界费用流 题意 这题题面搞得我看了半天没看懂--是这样的,原题中的"剧情"指的是边,"剧情点"指的才是点. 题面翻译过来大 ...