关于 Java 关键字 volatile 的总结
1 什么是 volatile
volatile 是 Java 的一个关键字,它提供了一种轻量级的同步机制。相比于重量级锁 synchronized,volatile 更为轻量级,因为它不会引起线程上下文的切换和调度。
2 volatile 的两个作用
可以禁止指令的重排序优化
提供多线程访问共享变量的内存可见性
3 禁止指令重排
3.1 什么是指令重排
指令重排序是 JVM 为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度,例如将多条指令并行执行或者是调整指令的执行顺序。但是在多线程的情况下,指令重排序可能会带来问题,例如程序执行的顺序可能会被调整。在加上 volatile 关键字之后可以有效解决这个问题。
下面我们举个例子:
double r = 2.1; //(1)
double pi = 3.14;//(2)
double area = pi*r*r;//(3)
在代码语句的顺序为 1->2->3,但实际上顺序无论是 1->2->3 还是 2->1->3 对结果并无影响,所以在编译时和运行时可以根据需要对1、2语句进行重排序。
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
1 不会对存在数据依赖关系的操作进行重排序
2 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
3.2 指令重排带来的问题
我们来看看这个基于双重检验的单例模式:
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();// 非原子操作
}
}
return instance;
}
}
事实上,这个单例模式的实现方式是有问题的,问题在哪呢?问题在于instance = new Singleton3(); 并不是一个原子操作。
我们可以将其抽象成以下几条指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
可以看到,操作2依赖于操作1,但操作3并不依赖于操作2。所以 JVM 是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
指令重排之后,instance 指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给 instance 引用,恰好另一个线程进入方法判断 instance 引用不为 null,然后就将其返回使用,导致出错。
3.3 禁止指令重排的原理
volatile 关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
内存屏障会确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。郑州哪家医院看妇科好 http://www.120zzkd.com/
对于上面的基于双重检验的单例模式,我们只需对其稍作修改即可令其正确运行。我们已经知道,问题来自于指令重排,那么我们禁止指令重排即可,用 volatile 关键字修饰 instance 变量,使得 instance 在读、写操作前后都会插入内存屏障,避免重排序。完整代码如下:
public class Singleton3 {
private static volatile Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized(Singleton3.class) {
if (instance == null)
instance = new Singleton3();
}
}
return instance;
}
}
4 保证内存可见性
4.1 什么是保证内存可见性
Java 支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。volatile 告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
4.2 实现的具体细节
如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
具体的说,内存可见性也是通过内存屏障实现的,它会执行下面两个操作:
强制将对缓存的修改操作立即写入主存
如果是写操作,它会导致其他 CPU 中对应的缓存行无效
5 总结
volatile 提供了一种轻量级的同步机制,在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞
volatile 只能确保可见性,而加锁机制既可以确保可见性又可以确保原子性
volatile 屏蔽掉了 JVM 中必要的代码优化,所以在效率上比较低
相比 synchronized,虽然 volatile 更简单并且开销更低,但它的同步性较差,而且其使用也更容易出错
关于 Java 关键字 volatile 的总结的更多相关文章
- JAVA关键字Volatile的特性
一.简述: 关键字Volatile是JAVA虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确.完整的理解,以致于许多程序员在遇到需要处理多线程数据竞争的时候一律使用synchronized来进 ...
- Java 关键字volatile的解释
volatile 关键字特征: 1.可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.可以禁止线程的工作内存对volatile修饰的变量进行缓存,并将修改的变量立即写入主存. 2. ...
- Java关键字-volatile
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制. 一旦某个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1.保证了不同线程对这个变 ...
- Java内存模型及Java关键字 volatile的作用和使用说明
先来看看这个关键字是什么意思:volatile [ˈvɒlətaɪl] adj. 易变的,不稳定的; 从翻译上来看,volatile表示这个关键字是极易发生改变的.volatile是java语言中, ...
- java 关键字volatile
一.Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作 ...
- java关键字volatile用法详解
volatile关键字想必大家都不陌生,在java 5之前有着挺大的争议,在java 5之后才逐渐被大家接受,同时作为java的关键字之一,其作用自然是不可小觑的,要知道它是java.util.con ...
- 深入汇编指令理解Java关键字volatile
volatile是什么 volatile关键字是Java提供的一种轻量级同步机制.它能够保证可见性和有序性,但是不能保证原子性 可见性 对于volatile的可见性,先看看这段代码的执行 flag默认 ...
- Java 关键字volatile 与 synchronized 作用与区别
1,volatile 它所修饰的变量不保留拷贝,直接访问主内存中的. 在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器).为了性能,一个线程会在自己 ...
- Java关键字volatile的实现原理(四)
简述 volatile 是轻量级的synchronized,在多线程开发中保证了共享变量的可见性.可见性就是当一个线程修改一个共享变量时,另一个线程可以读到修改的值.如果volatile变量使用恰当, ...
随机推荐
- 201871010110-李华《面向对象程序设计(java)》第十三周学习总结
项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...
- Linux安装Gitlab服务器
1. 下载GitLab 下载地址:https://packages.gitlab.com/gitlab/gitlab-ce/packages/el/7/gitlab-ce-10.8.2-ce.0.el ...
- Python进阶-VII 内置函数
一.内置函数引入 我们已经了解的有; print() input() range() next() dir() str() int() list() set() tuple() dict() he ...
- python之路—从入门到放弃
python基础部分 函数 初识函数 函数进阶 装饰器函数 迭代器和生成器 内置函数和匿名函数 递归函数 常用模块 常用模块 模块和包 面向对象 初识面向对象 面向对象进阶 网络编程 网络编程 并发编 ...
- withDefaultPasswordEncoder() 过时弃用问题
在学springsecurity5.X时,在demo里,内存配置用户的时候,提示withDefaultPasswordEncoder过时,特查看了源码,官方给出的理由是: /** @deprecate ...
- 解决 ora-01795 的问题
''' <summary> ''' 在 oracle 里 , where in 语句有可能造成问题 : ORA-01795:列表中的最大表达式数为1000 ''' 如果我们在拼接where ...
- centos7如何添加开机启动项?
centos7提供开启服务启动的方式: 1.系统服务管理命令,如果是通过yum安装的软件,开机启动脚本,已经自动创建好了,直接执行如下命令 nginx.service后缀可以省略 systemctl ...
- IdentityServer4实现原理
OAuth&OpenIDConnect是什么? 最近因为工作的原因,大概有两个月时间没写博客了,本来今年给自己的目标是每个月写一篇,或许记录工作中踩过的一些坑,或许学习一些新的技术框架.说实话 ...
- PowerShell的异常处理办法
$ErrorActionPreference = 'Stop' Try{ # C:\xxx 不存在 Copy-Item C:\xxx -ErrorAction Stop } Catch ...
- Java并发之原子操作类汇总
当程序更新一个变量时,如果是多线程同时更新这个变量,可能得到的结果与期望值不同.比如:有一个变量i,A线程执行i+1,B线程也执行i+1,经过两个线程的操作后,变量i的值可能不是期望的3,而是2.这是 ...