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 ...
随机推荐
- 剑指offer:包含min函数的栈
题目描述: 定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1)). 解题思路: 相当与在保留原栈的同时,去维护一个最小栈.利用一个辅助栈来完成.对于每个 ...
- java向上转型和向下转型
转型是在继承的基础上而言的,继承是面向对象语言中,代码复用的一种机制,通过继承,子类可以复用父类的功能,如果父类不能满足当前子类的需求,则子类可以重写父类中的方法来加以扩展. 向上转型:子类引用的对象 ...
- A总结
Alpha 答辩总结 评审表 组名 格式 内容 ppt 演讲 答辩 总计 天机组 15 15 14 15 14 73 PMS 16 16 15 15 16 78 日不落战队 16 16 16 15 1 ...
- 从苦逼到牛逼,详解Linux运维工程师的打怪升级之路
做运维也快四年多了,就像游戏打怪升级,升级后知识体系和运维体系也相对变化挺大,学习了很多新的知识点. 运维工程师是从一个呆逼进化为苦逼再成长为牛逼的过程,前提在于你要能忍能干能拼,还要具有敏锐的嗅觉感 ...
- Redis的相关问题总结
一.Redis的优缺点及适用场景 Redis 是一个基于内存的高性能key-value数据库.很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘 ...
- Java并发编程之深入理解线程池原理及实现
Java线程池在实际的应用开发中十分广泛.虽然Java1.5之后在JUC包中提供了内置线程池可以拿来就用,但是这之前仍有许多老的应用和系统是需要程序员自己开发的.因此,基于线程池的需求背景.技术要求了 ...
- Codeforces Round #485 Div. 1 vp记
A:对每种商品多源bfs一下每个点到该商品的最近距离,对每个点sort一下取前s个即可. #include<iostream> #include<cstdio> #includ ...
- FortiGate 硬件加速
FortiGate 硬件加速 来源 https://wenku.baidu.com/view/07749195a1c7aa00b52acb63.html 硬件加速 来源 https://blog.cs ...
- pandas read_csv 读取中文列标题文件报错
Traceback (most recent call last): File "C:/Users/arron/PycharmProjects/ML/ML/test.py", li ...
- BZOJ 3876 支线剧情 | 有下界费用流
BZOJ 3876 支线剧情 | 有下界费用流 题意 这题题面搞得我看了半天没看懂--是这样的,原题中的"剧情"指的是边,"剧情点"指的才是点. 题面翻译过来大 ...