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;
}

通过观察日志输出,发现有大量的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多线程环境指令重排的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java十分钟入门多线程中篇

    目录 1.线程的调度: 1.设置优先级(Priority): 2.休眠(sleep) 3.强制运行(join) 4.礼让(yield) 2.定时器线程: 3.线程的同步: 举例说明: 我们知道飞机在天上飞行是有固定的航线(可以理解成线程),每个机场都有最大的运行负载能力,当运行情况超过了负载能力的时候,这就需要塔台调度参与,会根据每架飞机的优先级排序.当在航线的时候,如果出现紧急情况,会让其他飞机避让,让这架飞机优先级提高,先降落.这就是调度,计算机程序线程运行也是这样的. 1.线程的调度: 在

  • Java多线程之如何确定线程数的方法

    关于多线程的线程数的确定,最近研读过几篇paper,在此做一下笔记,方便使用时翻看. 1.<Java 虚拟机并发编程>中介绍 就是说:线程数 = CPU的核心数 * (1 - 阻塞系数) 另一篇:<Java Concurrency in Practice>即<java并发编程实践>,给出的线程池大小的估算公式: Nthreads=Ncpu*Ucpu*(1+w/c),其中 Ncpu=CPU核心数,Ucpu=cpu使用率,0~1:W/C=等待时间与计算时间的比率 仔细推敲两

  • 浅谈java指令重排序的问题

    指令重排序是个比较复杂.觉得有些不可思议的问题,同样是先以例子开头(建议大家跑下例子,这是实实在在可以重现的,重排序的概率还是挺高的),有个感性的认识 /** * 一个简单的展示Happen-Before的例子. * 这里有两个共享变量:a和flag,初始值分别为0和false.在ThreadA中先给 a=1,然后flag=true. * 如果按照有序的话,那么在ThreadB中如果if(flag)成功的话,则应该a=1,而a=a*1之后a仍然为1,下方的if(a==0)应该永远不会为 * 真,

  • Java多线程之线程安全问题详解

    目录 1.什么是线程安全和线程不安全? 2.自增运算为什么不是线程安全的? 3.临界区资源和竞态条件 总结: 面试题: 什么是线程安全和线程不安全? 自增运算是不是线程安全的?如何保证多线程下 i++ 结果正确? 1. 什么是线程安全和线程不安全? 什么是线程安全呢?当多个线程并发访问某个Java对象时,无论系统如何调度这些线程,也无论这些线程将如何交替操作,这个对象都能表现出一致的.正确的行为,那么对这个对象的操作是线程安全的. 如果这个对象表现出不一致的.错误的行为,那么对这个对象的操作不是

  • Java中volatile防止指令重排

    目录 什么是指令重排? 为什么指令重排能够提高性能 volatile是怎么禁止指令重排的? volatile可以防止指令重排,在多线程环境下有时候我们需要使用volatile来防止指令重排,来保证代码运行后数据的准确性 什么是指令重排? 计算机在执行程序时,为了提高性能,编译器和处理器一般会进行指令重排,一般分为以下三种: 指令重排有以下三个特点: 1.单线程环境下指令重排后可以保证与顺序执行指令的结果一致(就是不进行指令重排的情况) //原来的执行顺序 a=1; b=0; //进行指令重排后执

  • Java volatile如何实现禁止指令重排

    计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种: 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令 单线程环境里面确保最终执行结果和代码顺序的结果一致 处理器在进行重排序时,必须要考虑指令之间的数据依赖性 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测. 指令重排 - example 1 public void mySort() { i

  • Java多线程之悲观锁与乐观锁

    目录 1. 悲观锁存在的问题 2. 通过CAS实现乐观锁 3. 不可重入的自旋锁 4. 可重入的自旋锁 总结 问题: 1.乐观锁和悲观锁的理解及如何实现,有哪些实现方式? 2.什么是乐观锁和悲观锁? 3.乐观锁可以重入吗? 1. 悲观锁存在的问题 独占锁其实就是一种悲观锁,java的synchronized是悲观锁.悲观锁可以确保无论哪个线程持有锁,都能独占式访问临界区.虽然悲观锁的逻辑非常简单,但是存在不少问题. 悲观锁总是假设会发生最坏的情况,每次线程读取数据时,也会上锁.这样其他线程在读取

  • Java十分钟入门多线程下篇

    目录 1.线程池: 2.创建线程池: 1.newCacheThreadPool: 2.newSingleThreadExecutor: 3.newFixedThreadPool(inta): 4.newScheduledTreadPool: 3.线程池创建自定义线程: 4.Runnable和Callable的区别: 5.线程池总结: 1.线程池: 什么是线程池? 咱们也不看长篇大论,通俗的来讲,线程池就是装线程的容器,当需要用的时候去池里面取出来,不用的时候放回去或者销毁.这样一个线程就可以反复

  • Java指令重排在多线程环境下的解决方式

    目录 一.序言 二.问题复原 (一)关联变量 1.结果预测 2.指令重排 (二)new创建对象 1.解析创建过程 2.重排序过程分析 三.应对指令重排 (一)AtomicReference原子类 (二)volatile关键字 四.指令重排的理解 1.指令重排广泛存在 2.多线程环境指令重排 3.synchronized锁与重排序无关 一.序言 指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响:在多线程环境下,指令重排会给程序带来意想不到的错误. 本文对多线程指令重排问题进行

  • Java指令重排序在多线程环境下的处理方法

    目录 一.序言 二.问题复原 (一)关联变量 1.结果预测 2.指令重排 (二)new创建对象 1.解析创建过程 2.重排序过程分析 三.应对指令重排 (一)AtomicReference原子类 (二)volatile关键字 1.指令重排广泛存在 2.多线程环境指令重排 3.synchronized锁与重排序无关 四.指令重排的理解 一.序言 指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响:在多线程环境下,指令重排会给程序带来意想不到的错误. 本文对多线程指令重排问题进行

  • Java多线程环境下SimpleDateFormat类安全转换

    一.SimpleDateFormat类 package state; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * SimpleDateFormat类负责日期的转换与格式化 * 解决SimpleDateFormat类多线程环境下转换错误问题 * @author zc * */ public class SimpleDateFormatThread e

  • Java多线程环境下死锁模拟

    目录 1.死锁产生的条件 2.模拟多线程环境下死锁的产生 3.死锁的排查 1.死锁产生的条件 互斥:一次只有一个进程可以使用一个资源.其他进程不能访问已分配给其他进程的资源. 不可抢占:不能抢占进程已占有的资源 请求和保持:当一个进程等待其他进程释放资源时,继续占有已经分配的资源 循环等待:存在一个封闭的进程链,使得每个进程至少占有此链中下一个进程所需要的一个资源. 注意:前三个条件都只是死锁存在的必要条件,但不是充分条件.第四个条件是充分条件.以上条件同样适用于线程. 2.模拟多线程环境下死锁

  • Linux多线程环境下 关于进程线程终止函数总结

    pthread_kill: pthread_kill与kill有区别,是向线程发送signal.,大部分signal的默认动作是终止进程的运行,所以,我们才要用signal()去抓信号并加上处理函数. int pthread_kill(pthread_t thread, int sig); 向指定ID的线程发送sig信号,如果线程代码内不做处理,则按照信号默认的行为影响整个进程,也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出. pthread

  • C#在复杂多线程环境下使用读写锁同步写入文件

    代码一: class Program { static int LogCount = 1000; static int SumLogCount = 0; static int WritedCount = 0; static int FailedCount = 0; static void Main(string[] args) { //往线程池里添加一个任务,迭代写入N个日志 SumLogCount += LogCount; ThreadPool.QueueUserWorkItem((obj)

  • 在Linux环境下采用压缩包方式安装JDK 13的方法

    什么是JDK? 好吧如果你不知道这个问题的话我实在是不知道你为什么要装这个东西. JDK(Java Development Kit)是Sun公司(后被Oracle收购)推出的面向对象程序设计语言的开发工具包,拥有这个工具包之后我们就可以使用Java语言进行程序设计和开发. 而今天我们要在Linux环境 下对这个东西进行部署以便能够进行开发,并且是以压缩包解压的方式进行安装,之所以不用rpm方式安装主要是为了能够在所有Linux系统上都通用,rpm和deb最多只能在Red Hat和Debian旗下

  • 详解Java枚举类在生产环境中的使用方式

    目录 前言 使用 1.确定业务场景状态 2.定义枚举类 3.自定义查询方法 4.测试效果 总结 前言   Java枚举在项目中使用非常普遍,许多人在做项目时,一定会遇到要维护某些业务场景状态的时候,往往会定义一个常量类,然后添加业务场景相关的状态常量.但实际上,生产环境的项目中业务状态的定义大部分是由枚举类来完成的,因为更加清晰明确,还能自定义不同的方法来获取对应的业务状态值,十分方便. 以下代码均为生产环境已上线项目的代码片段,仅供参考. 使用 大体分为确定业务场景状态.定义枚举类.自定义查询

  • Java中关于线程安全的三种解决方式

    三个窗口卖票的例子解决线程安全问题 问题:买票过程中,出现了重票.错票-->出现了线程的安全问题 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来,知道线程a操作完ticket时,其他线程才可以开始操作ticket,这种情况即使线程a出现了阻塞,也不能被改变 在Java中,我们通过同步机制,来解决线程的安全问题.(线程安全问题的前提:有共享数据) 方式一:同步代码块 synchroniz

  • Java String.replace()方法"无效"的原因及解决方式

    首先我们来看个例子 public class Demo1 { public static void main(String[] args) { String aa="abcd"; aa.replace("a","f"); System.out.println("输出结果是"+aa); } } 运行结果是什么呢?我们先看看这个方法的api 返回一个新的字符串,用newChar替换此字符串中出现的所有oldChar 所以这里的结果

随机推荐