volatile禁止重排优化
文章目录
- 简介
- 指令重排的危害
- 内存屏障(Memory Barrier)
- 机器层面(了解)
- JVM层面
- 验证volatile禁止指令重排
- 程序执行会出现指令重排
- 结论
- 解决指令重排
- 代码方式增加禁止指令重排
- 参考资料
简介
之前博客中说到并发编程三要素
、volatile无法保证原子性
、volatile保证可见性
以及指令重排
。本篇博客重点说明volatile
是如何禁止指令重排优化
的。
之前说到,volatile
是Java虚拟机提供的轻量级的同步机制。被volatile 关键字
修饰的共享变量
,对所有线程总数是可见的。
当一个线程修改了被volatile
修饰的共享变量
的值,新值总是会被其他线程感知。
除此之外,volatile
还能禁止计算机的指令重排优化
。
指令重排的危害
指令重排是计算机对命令执行效率的一种优化,但在有些时候,指令重排可能会带来某些意想不到的危害。
单利模式相信很多人都写过,就像下列懒汉式 单利
:
/*** 单例模式*/
public class SingleTest {// 类中提供实例对象private static SingleTest singleTest = null;// 构造方法私有,对外不允许采取new实例化对象private SingleTest() {}// 内部提供该类的实例化方式public static SingleTest getInstance(){if(singleTest == null){singleTest = new SingleTest();}return singleTest;}
}
上面的案例只是一个简单的单利模式,在单线程应用中并不会出现异常问题,如果是高并发环境下呢?
多个线程同时进入到
getInstance()
方法中,都同时判断到singleTest
对象为空,都会执行下列的实例化操作逻辑。
导致单例模式不在是真正意义上的单例模式。
为了避免在并发环境下,多线程也能创建和使用单例,通常针对上述案例,需要增加锁。比如增加java.util.concurrent.locks.Lock
或者synchronized
等。
当前,只采取synchronized
进行说明,修改后的代码案例如下所示:
import lombok.extern.slf4j.Slf4j;/*** 单例模式*/
@Slf4j
public class SingleTest {// 类中提供实例对象private static SingleTest singleTest = null;// 构造方法私有,对外不允许采取new实例化对象private SingleTest() {}// 内部提供该类的实例化方式public static SingleTest getInstance(){// 初步判断if(singleTest == null){synchronized (SingleTest.class){// 加锁后再次判断if(singleTest == null){singleTest = new SingleTest();}}}return singleTest;}
}
添加了synchronized
关键字后的当前单例模式相比之前的单利类而言,安全了很多。但在多线程环境下,依旧有极低几率出现创建多个对象的现象。
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
【疑问:】为什么依旧会出现上述情况?
这个问题的根源,还需要从实例化SingleTest
类的过程来分析。
在Java代码执行前,需要经过编译成机器可识别的命令,在对象的创建中,大致分为三步操作,查看字节码文件。
// 观察一个类在创建时的过程
class Tests{public static void main(String[] args) {new Object();}
}
javap -c Tests.class
大致步骤如下所示:
1、在内存中对待实例化的对象,分配空间。
2、初始化对象。
3、将空间的地址赋值给引用的变量。
伪代码如下:
memory = allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
singleTest = memory;//3.设置instance指向刚分配的内存地址,此时singleTest !=null
在高并发环境下,多个线程同时调用一个单利类的实例化方式,计算机为了提高cpu执行效率,可能会在步骤1
和步骤2
中进行指令重排。重排后的伪代码如下所示:
memory = allocate();//1.分配对象内存空间
singleTest = memory;//3.设置instance指向刚分配的内存地址,此时singleTest !=null,但是对象还未进行初始化操作
instance(memory);//2.初始化对象
在单线程环境
下,步骤2和步骤3之间不存在数据依赖关系
,无论重排前还是重排后,其执行结果是不会改变,是允许的。
但是,指令重排,只会保证
串行语句
执行的一致性
;
并不会
关心多线程间的语义一致性。
所以,在多线程环境下
,当某一条线程访问到singleTest != null
时,由于instance(memory)
此时并未初始化完成
,当其他线程获取到singleTest != null
时,假设调用该实例对象中
的某个方法
时,就会出现报错!
只是申请了空间,但还未给这块空间中注入该类的一些信息。如:成员变量、成员方法。
还只是一块空的空间
。
【总结:】必须加volatile修饰!
所以此时为了解决上述指令重排带来的问题,必须在共享变量上增加volatile 关键字
来禁止计算机的指令重排操作。
修改后的案例如下所示:
private volatile static SingleTest singleTest = null;
那么,volatile是如何保证禁止计算机指令重排优化的呢?
首先需要了解一个概念:内存屏障(Memory Barrier)
。
内存屏障(Memory Barrier)
机器层面(了解)
在Intel硬件提供了一系列内存屏障,主要有:
- 1、lfence,是一种Load Barrier 读屏障
- 2、sfence, 是一种Store Barrier 写屏障
- 3、mfence, 是一种全能型的屏障,具备ifence和sfence的能力
- 4、 Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。
Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD,ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR,SBB, SUB, XOR, XADD, and XCHG等指令。
JVM层面
不同的硬件实现内存屏障的方式不同,但JMM屏蔽了这种底层硬件平台的差异。由JVM针对不同的平台生成对应的机器码。
其中,JVM 提供了四类内存屏障指令:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad |
Load1; LoadLoad ; Load2
|
load1读操作与load2读操作,不允许重排。 (load2不能放在load1之前执行) |
StoreStore |
Store1;StoreStore ; Store2
|
第一个写store1,与第二个写store2之间不允许重排。 (store2不能在store1之前执行) |
LoadStore |
Load1;LoadStore ; Store2
|
写入操作,不能在读操作之前执行 |
StoreLoad |
Store1; StoreLoad ; Load2
|
读操作,不能在写操作之前执行 |
验证volatile禁止指令重排
程序执行会出现指令重排
编写Java代码,测试是否会出现指令重排,案例如下所示:
import lombok.extern.slf4j.Slf4j;@Slf4j
public class CodeReorder {private static int x = 0, y = 0;private static int a = 0, b = 0;public static void main(String[] args) throws InterruptedException {int i = 0;for (;;){ // 这是一个死循环i++;x = 0; y = 0;a = 0; b = 0;Thread t1 = new Thread(new Runnable() {public void run() {shortWait(10000);a = 1;x = b;}});Thread t2 = new Thread(new Runnable() {public void run() {b = 1;y = a;}});// 开启线程t1.start();t2.start();// 等待各自的线程销毁t1.join();t2.join();// 输出此时的赋值信息,并进行下一轮循环// 不考虑指令重排String result = "第" + i + "次 (" + x + "," + y + ")";if(x == 0 && y == 0) {System.out.println(result);break;} else {log.info(result);}}}/*** 等待一段时间,时间单位纳秒* @param interval*/public static void shortWait(long interval){long start = System.nanoTime();long end;do{end = System.nanoTime();}while(start + interval >= end);}
}
上述的代码中,针对初始变量信息包含x
、y
、a
、b
。且初始值都为0。
使用死循环,执行多个操作,分别对上述共享变量进行赋值操作,并在线程死亡时,输出当前已赋值的信息。
不考虑指令重排的前提下
,上述执行结果可能会有以下几种类型:
线程是有
中断
现象的。
1、线程1先执行,线程2后执行
2、线程2先执行,线程1后执行
3、线程1先执行,但中断了,线程2开始执行,线程2执行完成后,线程1恢复继续执行。
如果上述代码在执行时,不会出现指令重排
时,结果不会出现x = 0; y = 0 ;
的现象。
如果出现了
x = 0; y = 0 ;
,则表示程序在执行时,会被指令重排!
执行上述代码,查看返回结果信息,如下所示:
结论
程序在并发情况,执行过程中会出现指令重排现象!
解决指令重排
只需要在共享变量前,添加volatile 关键字
修饰即可,如下所示:
代码方式增加禁止指令重排
在使用volatile关键字
修饰共享变量时,JVM会自动向程序中添加内存屏障
的方式,禁止指令重排。
除了采取volatile
让JVM自动处理之外,也能通过手动增加代码的方式,避免程序在执行时,CPU对其进行指令重排优化操作。
在Java中,有一个sun.misc.Unsafe
类,可以使用其进行手动添加内存屏障
来解决。
其中,供使用的方法有如下几种:
由于这个类是一个单利模式的类
。
并不能直接通过new Unsafe()
的方式获取其实例化对象
,只能通过反射获取。其获取方式如下所示:
import sun.misc.Unsafe;
import java.lang.reflect.Field;public class UnsafeInstance {public static Unsafe reflectGetUnsafe() {try {// 根据属性名称获取具体的属性Field field = Unsafe.class.getDeclaredField("theUnsafe");// 去除校验field.setAccessible(true);// 返回该属性的具体值return (Unsafe) field.get(null);} catch (Exception e) {e.printStackTrace();}return null;}}
然后,在具体需要防止执行重排的代码之间添加如下代码即可:
UnsafeInstance.reflectGetUnsafe().fullFence();
如图:
参考资料
java.lang.reflect.Field.get()方法示例
java反射知识总结(只针对使用)
volatile禁止重排优化相关推荐
- Volatile禁止指令重排
Volatile禁止指令重排 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种: 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系 ...
- volatile指令重排_有多少人面试栽到Volatile上?面试问题都总结到这儿了
Volatile关键字 volatile 是Java虚拟机提供的 轻量级 的同步机制.何为 轻量级 呢,这要相对于 synchronized 来说.Volatile有如下三个特点. 要搞清楚上面列举的 ...
- volatile指令重排_面试:为了进阿里,重新翻阅了Volatile与Synchro
面试:为了进阿里,重新翻阅了Volatile与Synchronized 在深入理解使用Volatile与Synchronized时,应该先理解明白Java内存模型 (Java Memory Model ...
- volatile指令重排_学会了volatile,你变心了,我看到了
volatile 简介 一般用来修饰共享变量,保证可见性和可以禁止指令重排 多线程操作同一个变量的时候,某一个线程修改完,其他线程可以立即看到修改的值,保证了共享变量的可见性 禁止指令重排,保证了代码 ...
- volatile指令重排_volatile可见性和指令重排
volatile关键字的2个作用 1.线程的可见性 2.防止指令重排 什么是线程的可见性? 线程的可见性 就是一个线程对一个变量进行更改操作 其他线程获取会获得最新的值. 线程在执行的行 操作主线程的 ...
- volatile禁止重排序详解
首先说明本文并不是讲解volatile不保证原子性.如何保证可见性xxxx,还不懂的请参考 让你彻底理解volatile 并发关键字volatile(重排序和内存屏障) 本文针对以下两个问题解答 1. ...
- mysql禁止自动优化_MySQL必须调整的10项配置优化
即使是经验老道的人也会犯错,会引起很多麻烦.所以在盲目的运用这些推荐之前,请记住下面的内容: 一次只改变一个设置!这是测试改变是否有益的唯一方法. 大多数配置能在运行时使用SET GLOBAL改变.这 ...
- 一文读懂Java内存模型(JMM)及volatile关键字
点赞再看,养成习惯,公众号搜一搜[一角钱技术]关注更多原创技术文章. 本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章. 前言 并发编程从操作系统底层工作的整 ...
- 全面理解Java内存模型(JMM)及volatile关键字
[版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...
最新文章
- 几种常见窗函数及其MATLAB程序实现
- C++ NULL指针学习 - Win32版本
- Flink从入门到精通100篇(十九)-基于 Flink 的大规模准实时数据分析平台的建设实践
- QQ视频直播架构及原理
- PHP OPCode缓存:APC详细介绍
- 在Maven仓库中添加Oracle JDBC驱动
- SpringMVC莫名其妙出现No bean named 'cacheManager' is defined错误
- 【华为云技术分享】【昇腾】【玩转Atlas200DK系列】基于Pycharm专业版构建开发板python开发运行环境
- 【带着canvas去流浪(11)】Three.js入门学习笔记
- LeetCode 144. 二叉树的前序遍历(递归)(迭代)(颜色标记法)
- 初识delphi-spring-framework
- 谈谈工作和学习中,所谓的主动性
- 将Excel的数据导入DataGridView中[原创]
- 小明种苹果(续)第十七次CCF认证
- 简单几招,教你将GIF动图转换为JPG图片
- 计算机老师教育感言,教育信息技术培训心得感言
- java win7 管理员权限_获得WIN7管理员权限(可通过修改注册表,或者组策略改变)...
- 最强GTD时间管理工具:OmniFocus Pro 3 for Mac支持big sur
- 思成五笔的通俗易懂讲解
- HTML语言分栏左右比例怎么调整,wps怎么设置分栏排版?