Java内存模型-volatile的内存语义
一 引言
听说在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的内存语义的更多相关文章
- 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则
前言 楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中.在一次学习中去理解这些东西.才能更好的理解 Jav ...
- java内存模型-volatile
volatile 的特性 当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别.理解 volatile 特性的一个好方法是:把对 volatile 变量的单个读/写,看成是使用同一 ...
- 深入理解Java内存模型 - volatile
volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这 ...
- 深入理解Java内存模型——volatile
volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会非常特别. 理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁 ...
- 【java多线程系列】java中的volatile的内存语义
在java的多线程编程中,synchronized和volatile都扮演着重要的 角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性,可见性指的是当一 ...
- JAVA锁和volatile的内存语义&volatile的使用场景
JAVA锁的内存语义 当线程释放锁时,JMM(Java Memory Model)会把该线程对应的本地内存中的共享变量刷新到主内存中. 当线程获取锁时,JMM会将该线程对应的本地内存置为无效.从而使得 ...
- Java并发编程里的volatile。Java内存模型核CPU内存架构的对应关系
CPU内存架构:https://www.jianshu.com/p/3d1eb589b48e Java内存模型:https://www.jianshu.com/p/27a9003c33f4 多线程下的 ...
- Java内存模型-锁的内存语义
一 引言 在说volatile的内存语义时,讲过这样一句话:想要理解透volatile特性有一个很好的方法,就是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.所 ...
- JMM内存模型+volatile+synchronized+lock
硬件内存模型: Java内存模型: 每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个内存共享. 下面 8 个操作都是原子的,不可再分的: 1) lock ...
随机推荐
- 《Linux内核分析》第三周学习小结 构造一个简单的Linux系统OS
郝智宇 无转载 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 第三周 构造一个简单的Linux系统Me ...
- 移动的调试工具vConsole
在PC端写代码调试的时候,直接console.log()即可,但是在手机端怎么调试??最近发现一个很有用的插件vConsole 首先引入插件: 然后再文件中使用即可: 这样再手机中就会出现下面的标识, ...
- delphi 登陆窗口的问题 [问题点数:30分,结帖人tianhuo_soft]
http://bbs.csdn.net/topics/200053917 有两个窗口formLogin是登陆窗口,formMain是主窗口我想让formMain显示出来,并且formLogin窗口关闭 ...
- asp.net使用动态模版导出word
具体思路: 1.先制作Word模版,使用文本框+书签的方式来设计模版: 2.模版制作完之后,根据模版生成新文件,使用File.Copy方法,生成.doc格式新文件: 3.后台取得数据,参照网页渲染的方 ...
- MySql的相关资操作
01-MySql的前戏 MySql的前戏 在学习Mysql之前,我们先来想一下一开始做的登录注册案例,当时我们把用户的信息保存到一个文件中: #用户名 |密码root|123321 alex|12 ...
- 9:@RequestMapping 用法详解之地址映射
引言: 前段时间项目中用到了RESTful模式来开发程序,但是当用POST.PUT模式提交数据时,发现服务器端接受不到提交的数据(服务器端参数绑定没有加任何注解),查看了提交方式为applicatio ...
- c++并发编程之thread::join()和thread::detach()
thread::join(): 阻塞当前线程,直至 *this 所标识的线程完成其执行.*this 所标识的线程的完成同步于从 join() 的成功返回. 该方法简单暴力,主线程等待子进程期间什么都不 ...
- 界面编程之QT的线程20180731
/*******************************************************************************************/ 一.为什么需 ...
- Python基础学习(三)
了解了Python的基础使用,接下来继续练手廖雪峰老师的教学案例. 一.变量可以指向函数 说明,一个函数可以赋值给一个变量,该变量就会具有该函数的功能,举例: gg = abs print( gg(- ...
- 项目实战SportsStore——订单处理模块
前面的步骤如果顺利完成,你的网站运行之后应该能够正常显示下面三个页面: 1.产品列表 2.购物车内容页面 在某个商品后面点击“添加到购物车”则出现下图页面: 此页面上点击“继续购物”按钮则返回到产品列 ...