Java指令重排序在多线程环境下的应对策略
一、序言
指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响;在多线程环境下,指令重排会给程序带来意想不到的错误。
本文对多线程指令重排问题进行复原,并针对指令重排给出相应的解决方案。
二、问题复原
(一)关联变量
下面给出一个能够百分之百复原指令重排的例子。
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;
}
通过观察日志输出,发现有大量的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
@AllArgsConstructor
class UserModel {
private Integer userId;
private String userName;
}
1、解析创建过程
- 使用关键字new创建一个对象,大致分为一下过程:
- 在栈空间创建引用地址
- 以类文件为模版在堆空间对象分配内存
- 成员变量初始化
- 使用构造函数初始化
- 将引用值赋值给左侧存储变量
2、重排序过程分析
针对上述示例,假设第一个线程进入synchronized代码块,并开始创建对象,由于重排序存在,正常的创建对象过程被打乱,可能会出现在栈空间创建引用地址后,将引用值赋值给左侧存储变量,随后因CPU调度时间片耗尽而产生中断的情况。
后续线程在检测到instance
变量不为空,则直接使用。因为单例对象并为实例化完成,直接使用会带来意想不到的结果。
三、应对指令重排
(一)AtomicReference原子类
使用原子类将一组相关联的变量封装成一个对象,利用原子操作的特性,有效回避指令重排问题。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ValueModel {
private Integer value;
private Boolean flag;
}
原子类应该是解决多线程环境下指令重排的首选方案,不仅通俗易懂,而且线程间使用的非重量级互斥锁,效率相对较高。
public class E {
private static final AtomicReference<ValueModel> ar = 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
@AllArgsConstructor
class UserModel {
private Integer userId;
private String userName;
}
四、指令重排的理解
1、指令重排广泛存在
指令重排不仅限于Java程序,实际上各种编译器均有指令重排的操作,从软件到CPU硬件都有。指令重排是对单线程执行的程序的一种性能优化,需要明确的是,指令重排在单线程环境下,不会改变顺序程序执行的预期结果。
2、多线程环境指令重排
上面讨论了两种典型多线程环境下指令重排,分析其带来负面影响,并分别提供了应对方式。
- 对于关联变量,先封装成一个对象,然后使用原子类来操作
- 对于new对象,使用volatile关键字修饰目标对象即可
3、synchronized锁与重排序无关
synchronized锁通过互斥锁,有序的保证线程访问特定的代码块。代码块内部的代码正常按照编译器执行的策略重排序。
尽管synchronized锁能够回避多线程环境下重排序带来的不利影响,但是互斥锁带来的线程开销相对较大,不推荐使用。
synchronized 块里的非原子操作依旧可能发生指令重排
Java指令重排序在多线程环境下的应对策略的更多相关文章
- java指令重排序的问题
转载自于:http://my.oschina.net/004/blog/222069?fromerr=ER2mp62C 指令重排序是个比较复杂.觉得有些不可思议的问题,同样是先以例子开头(建议大家跑下 ...
- Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- 多线程编程:一个指令重排序引发的chaos
先贴出正确的代码: package com.xiaobai.thread.main; import lombok.extern.slf4j.Slf4j; @Slf4j public class Thr ...
- 不得不提的volatile及指令重排序(happen-before)
微信公众号[程序员江湖] 作者黄小斜,斜杠青年,某985硕士,阿里 Java 研发工程师,于 2018 年秋招拿到 BAT 头条.网易.滴滴等 8 个大厂 offer,目前致力于分享这几年的学习经验. ...
- 轻松学JVM(二)——内存模型、可见性、指令重排序
上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...
- JVM学习--(二)内存模型、可见性、指令重排序
我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存模型 首先我们思考一下一个java线程要向另外一个线程进行通信,应该怎么做,我们再 ...
- 深入理解JVM(二)——内存模型、可见性、指令重排序
上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...
- 【java多线程系列】java内存模型与指令重排序
在多线程编程中,需要处理两个最核心的问题,线程之间如何通信及线程之间如何同步,线程之间通信指的是线程之间通过何种机制交换信息,同步指的是如何控制不同线程之间操作发生的相对顺序.很多读者可能会说这还不简 ...
随机推荐
- 学习廖雪峰的git教程6--版本控制
1git log 查看提交的版本 2git reset --hard HEAD^回退到上一个版本 3git reset --hard 版本号 回到某一个版本
- Linux 系统下你关注过哪些内核参数,说说你知道的?
Tcp/ip io cpu memorynet.ipv4.tcp_syncookies = 1#启用syncookiesnet.ipv4.tcp_max_syn_backlog = 8192#SYN队 ...
- 什么是 Executors 框架?
Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框 架. 无限制的创建线程会引起应用程序内存溢出.所以创建一个线程池是个更好的的 解决方案,因为可以限制线程的数量并且可以 ...
- python 元组tuple 增删改查操作
初始化: data_tuple = () data_tuple1 = (1,) data_tuple2 = tuple() 新增: data_tuple+data_tuple1 (data_tuple ...
- docker安装部署、fastDFS文件服务器搭建与springboot项目接口
一.docker安装部署 1.更新yum包:sudo yum update 2.安装需要的软件包,yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动 ...
- Java 中你怎样唤醒一个阻塞的线程?
在 Java 发展史上曾经使用 suspend().resume()方法对于线程进行阻塞唤醒,但 随之出现很多问题,比较典型的还是死锁问题. 解决方案可以使用以对象为目标的阻塞,即利用 Object ...
- 使用redis作为django缓存数据库
1.Redis的Windows版本.打开https://github.com/MSOpenTech/redis/releases下载msi安装包.该版本是64位.安装msi过程中,有个选项是否加入系统 ...
- 学习RabbitMQ(一)
消息中间件 一.简介 消息中间件就是在消息的传输过程中保存消息的容器.消息中间件再将消息从它的源中继到它的目标时充当中间人的作用.队列的主要目的是提供路由并保证消息的传递:如果发送消息时接收者不可用, ...
- mybatis-01-基本流程
mybatis执行流程 1. 加载配置文件并初始化(SqlSession) 配置文件来源于两个地方,一个是配置文件(主配置文件conf.xml,mapper文件*.xml), 一个是java代码中 ...
- 使用 vscode 插件可视化制作和管理脚手架及原理解析
提到脚手架,大家想到的可能就是各种 xxx-cli,本文介绍的是另一种方式:以 vscode 插件的形式实现,提供 web 可视化操作,如下图: 下面介绍如何安装使用,以及实现原理. 安装使用 vsc ...