好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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

一、序言

指令重排在单线程环境下有利于提高程序的执行效率,不会对程序产生负面影响;在多线程环境下,指令重排会给程序带来意想不到的错误。

本文对多线程指令重排问题进行复原,并针对指令重排给出相应的解决方案。

二、问题复原

(一)关联变量

下面给出一个能够百分之百复原指令重排的例子。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

ublic 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 在两个方法类均存在指令重排的情况。

?

1

2

3

4

public static void writer() {

     a = 1 ;

     flag = true ;

}

通过观察日志输出,发现有大量的0输出。

当 writer 方法内部发生指令重排时, flag 变量先完成赋值,此时假如当前线程发生中断,其它线程在调用 reader 方法,检测到 flag 变量为true,那么便打印变量 a 的值。此时控制台存在超出期望值的结果。

(二)new创建对象

使用关键字new创建对象时,因其非原子操作,故存在指令重排,指令重排在多线程环境下会带来负面影响。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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原子类

使用原子类将一组相关联的变量封装成一个对象,利用原子操作的特性,有效回避指令重排问题。

?

1

2

3

4

5

6

7

@Data

@NoArgsConstructor

@AllArgsConstructor

public class ValueModel {

     private Integer value;

     private Boolean flag;

}

原子类应该是解决多线程环境下指令重排的首选方案,不仅通俗易懂,而且线程间使用的非重量级互斥锁,效率相对较高。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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关键字

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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指令重排序内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

原文链接:https://www.cnblogs.com/javazhishitupu/p/16181954.html

查看更多关于Java指令重排序在多线程环境下的处理方法的详细内容...

  阅读:13次