Java指令重排在多线程环境下的解决方式
目录
- 一、序言
- 二、问题复原
- (一)关联变量
- 1、结果预测
- 2、指令重排
- (二)new创建对象
- 1、解析创建过程
- 2、重排序过程分析
- 三、应对指令重排
- (一)AtomicReference原子类
- (二)volatile关键字
- 四、指令重排的理解
- 1、指令重排广泛存在
- 2、多线程环境指令重排
- 3、synchronized锁与重排序无关
一、序言 指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响;在多线程环境下,指令重排会给程序带来意想不到的错误。
本文对多线程指令重排问题进行复原,并针对指令重排给出相应的解决方案。
二、问题复原
(一)关联变量
下面给出一个能够百分之百复原指令重排的例子。
public class D {static Integer a; static Boolean flag; public static void writer() {a = 1; flag = true; }public static void reader() {if (flag != null && flag) {System.out.println(a); a = 0; flag = false; }}}
1、结果预测
reader
方法仅在flag
变量为true时向控制台打印变量a
的值。writer
方法先执行变量a
的赋值操作,后执行变量flag
的赋值操作。如果按照上述分析逻辑,那么控制台打印的结果一定全为1。
2、指令重排 假如代码未发生指令重排,那么当
flag
变量为true时,变量a
一定为1。上述代码中关于变量
a
和变量flag
在两个方法类均存在指令重排的情况。public static void writer() {a = 1; flag = true; }
【Java指令重排在多线程环境下的解决方式】通过观察日志输出,发现有大量的0输出。
当
writer
方法内部发生指令重排时,flag
变量先完成赋值,此时假如当前线程发生中断,其它线程在调用reader
方法,检测到flag
变量为true,那么便打印变量a
的值。此时控制台存在超出期望值的结果。(二)new创建对象
使用关键字new创建对象时,因其非原子操作,故存在指令重排,指令重排在多线程环境下会带来负面影响。
public class Singleton {private static UserModel instance; public static UserModel getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new UserModel(2, "B"); }}}return instance; }}@Data@AllArgsConstructorclass UserModel {private Integer userId; private String userName; }
1、解析创建过程
- 使用关键字new创建一个对象,大致分为一下过程:
- 在栈空间创建引用地址
- 以类文件为模版在堆空间对象分配内存
- 成员变量初始化
- 使用构造函数初始化
- 将引用值赋值给左侧存储变量
2、重排序过程分析 针对上述示例,假设第一个线程进入synchronized代码块,并开始创建对象,由于重排序存在,正常的创建对象过程被打乱,可能会出现在栈空间创建引用地址后,将引用值赋值给左侧存储变量,随后因CPU调度时间片耗尽而产生中断的情况。
后续线程在检测到
instance
变量不为空,则直接使用。因为单例对象并为实例化完成,直接使用会带来意想不到的结果。三、应对指令重排
(一)AtomicReference原子类
使用原子类将一组相关联的变量封装成一个对象,利用原子操作的特性,有效回避指令重排问题。
@Data@NoArgsConstructor@AllArgsConstructorpublic class ValueModel {private Integer value; private Boolean flag; }
原子类应该是解决多线程环境下指令重排的首选方案,不仅通俗易懂,而且线程间使用的非重量级互斥锁,效率相对较高。
public class E {private static final AtomicReferencear = new AtomicReference<>(new ValueModel()); public static void writer() {ar.set(new ValueModel(1, true)); }public static void reader() {ValueModel valueModel = ar.get(); if (valueModel.getFlag() != null && valueModel.getFlag()) {System.out.println(valueModel.getValue()); ar.set(new ValueModel(0, false)); }}}
当一组相关联的变量发生指令重排时,使用原子操作类是比较优的解法。
(二)volatile关键字
public class Singleton {private volatile static UserModel instance; public static UserModel getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new UserModel(2, "B"); }}}return instance; }}@Data@AllArgsConstructorclass UserModel {private Integer userId; private String userName; }
四、指令重排的理解
1、指令重排广泛存在
指令重排不仅限于Java程序,实际上各种编译器均有指令重排的操作,从软件到CPU硬件都有。指令重排是对单线程执行的程序的一种性能优化,需要明确的是,指令重排在单线程环境下,不会改变顺序程序执行的预期结果。
2、多线程环境指令重排
上面讨论了两种典型多线程环境下指令重排,分析其带来负面影响,并分别提供了应对方式。
- 对于关联变量,先封装成一个对象,然后使用原子类来操作
- 对于new对象,使用volatile关键字修饰目标对象即可
3、synchronized锁与重排序无关
synchronized锁通过互斥锁,有序的保证线程访问特定的代码块。代码块内部的代码正常按照编译器执行的策略重排序。
尽管synchronized锁能够回避多线程环境下重排序带来的不利影响,但是互斥锁带来的线程开销相对较大,不推荐使用。
synchronized 块里的非原子操作依旧可能发生指令重排到此这篇关于Java多线程环境指令重排的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
推荐阅读
- Java|Java 九宫重排(满分解法)
- 如何使用Wavesurfer.js从JavaScript中的音频文件生成音频波(音频频谱)
- 如何从JavaScript中的2个值计算百分比变化(增加和减少)
- 使用Mousetrap在JavaScript中添加键盘快捷键的正确方法
- 掌握Linux这30个常用指令,帮你解决95%以上的问题
- 如何在Python 3中从SRT文件中重新同步电影的字幕(移位)
- 你是否应该专门花时间学习Java编程语言()
- 如何在JavaScript中将图像合并为QR码
- 如何使用JavaScript突出显示Google地图中的区域(城市,州或国家/地区)
- 如何使用JavaScript从URL检索所有或特定的get参数