文章目录

  • 简介
  • 指令重排的危害
  • 内存屏障(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);}
}

上述的代码中,针对初始变量信息包含xyab。且初始值都为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禁止重排优化相关推荐

  1. Volatile禁止指令重排

    Volatile禁止指令重排 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种: 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系 ...

  2. volatile指令重排_有多少人面试栽到Volatile上?面试问题都总结到这儿了

    Volatile关键字 volatile 是Java虚拟机提供的 轻量级 的同步机制.何为 轻量级 呢,这要相对于 synchronized 来说.Volatile有如下三个特点. 要搞清楚上面列举的 ...

  3. volatile指令重排_面试:为了进阿里,重新翻阅了Volatile与Synchro

    面试:为了进阿里,重新翻阅了Volatile与Synchronized 在深入理解使用Volatile与Synchronized时,应该先理解明白Java内存模型 (Java Memory Model ...

  4. volatile指令重排_学会了volatile,你变心了,我看到了

    volatile 简介 一般用来修饰共享变量,保证可见性和可以禁止指令重排 多线程操作同一个变量的时候,某一个线程修改完,其他线程可以立即看到修改的值,保证了共享变量的可见性 禁止指令重排,保证了代码 ...

  5. volatile指令重排_volatile可见性和指令重排

    volatile关键字的2个作用 1.线程的可见性 2.防止指令重排 什么是线程的可见性? 线程的可见性 就是一个线程对一个变量进行更改操作 其他线程获取会获得最新的值. 线程在执行的行 操作主线程的 ...

  6. volatile禁止重排序详解

    首先说明本文并不是讲解volatile不保证原子性.如何保证可见性xxxx,还不懂的请参考 让你彻底理解volatile 并发关键字volatile(重排序和内存屏障) 本文针对以下两个问题解答 1. ...

  7. mysql禁止自动优化_MySQL必须调整的10项配置优化

    即使是经验老道的人也会犯错,会引起很多麻烦.所以在盲目的运用这些推荐之前,请记住下面的内容: 一次只改变一个设置!这是测试改变是否有益的唯一方法. 大多数配置能在运行时使用SET GLOBAL改变.这 ...

  8. 一文读懂Java内存模型(JMM)及volatile关键字

    点赞再看,养成习惯,公众号搜一搜[一角钱技术]关注更多原创技术文章. 本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章. 前言 并发编程从操作系统底层工作的整 ...

  9. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

最新文章

  1. 几种常见窗函数及其MATLAB程序实现
  2. C++ NULL指针学习 - Win32版本
  3. Flink从入门到精通100篇(十九)-基于 Flink 的大规模准实时数据分析平台的建设实践
  4. QQ视频直播架构及原理
  5. PHP OPCode缓存:APC详细介绍
  6. 在Maven仓库中添加Oracle JDBC驱动
  7. SpringMVC莫名其妙出现No bean named 'cacheManager' is defined错误
  8. 【华为云技术分享】【昇腾】【玩转Atlas200DK系列】基于Pycharm专业版构建开发板python开发运行环境
  9. 【带着canvas去流浪(11)】Three.js入门学习笔记
  10. LeetCode 144. 二叉树的前序遍历(递归)(迭代)(颜色标记法)
  11. 初识delphi-spring-framework
  12. 谈谈工作和学习中,所谓的主动性
  13. 将Excel的数据导入DataGridView中[原创]
  14. 小明种苹果(续)第十七次CCF认证
  15. 简单几招,教你将GIF动图转换为JPG图片
  16. 计算机老师教育感言,教育信息技术培训心得感言
  17. java win7 管理员权限_获得WIN7管理员权限(可通过修改注册表,或者组策略改变)...
  18. 最强GTD时间管理工具:OmniFocus Pro 3 for Mac支持big sur
  19. 思成五笔的通俗易懂讲解
  20. HTML语言分栏左右比例怎么调整,wps怎么设置分栏排版?

热门文章

  1. 海报教程,制作楚乔传火焰海报
  2. 轻量级聊天应用VoceChat
  3. 机器学习之常见的性能度量
  4. 用webstorm搭建vue项目(亲测,绝对实用)
  5. Unity3D实现2D人物动画① UGUINative2D序列帧动画
  6. 01_人工智能与机器学习概念介绍
  7. linux 剪切命令 mv
  8. 乔布斯传名言(笔记)——Goging Pubilc
  9. 计算机三维辅助设计3DMaX,计算机三维辅助设计小结.doc凤仔
  10. socket编程的 sendto 函数