欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

浅谈java指令重排序的问题

程序员文章站 2024-02-27 17:46:51
指令重排序是个比较复杂、觉得有些不可思议的问题,同样是先以例子开头(建议大家跑下例子,这是实实在在可以重现的,重排序的概率还是挺高的),有个感性的认识 /**...

指令重排序是个比较复杂、觉得有些不可思议的问题,同样是先以例子开头(建议大家跑下例子,这是实实在在可以重现的,重排序的概率还是挺高的),有个感性的认识

/**
 * 一个简单的展示happen-before的例子.
 * 这里有两个共享变量:a和flag,初始值分别为0和false.在threada中先给  a=1,然后flag=true.
 * 如果按照有序的话,那么在threadb中如果if(flag)成功的话,则应该a=1,而a=a*1之后a仍然为1,下方的if(a==0)应该永远不会为
 * 真,永远不会打印.
 * 但实际情况是:在试验100次的情况下会出现0次或几次的打印结果,而试验1000次结果更明显,有十几次打印.
 */
public class simplehappenbefore {
 /** 这是一个验证结果的变量 */
 private static int a=0;
 /** 这是一个标志位 */
 private static boolean flag=false;
 public static void main(string[] args) throws interruptedexception {
  //由于多线程情况下未必会试出重排序的结论,所以多试一些次
  for(int i=0;i<1000;i++){
   threada threada=new threada();
   threadb threadb=new threadb();
   threada.start();
   threadb.start();
   //这里等待线程结束后,重置共享变量,以使验证结果的工作变得简单些.
   threada.join();
   threadb.join();
   a=0;
   flag=false;
  }
 }
 static class threada extends thread{
  public void run(){
  a=1;
  flag=true;
  }
 }
 static class threadb extends thread{
  public void run(){
   if(flag){
   a=a*1;
   }
   if(a==0){
   system.out.println("ha,a==0");
   }
  }
 }
}

例子比较简单,也添加了注释,不再详细叙述。

什么是指令重排序?有两个层面:

在虚拟机层面,为了尽可能减少内存操作速度远慢于cpu运行速度所带来的cpu空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用cpu。拿上面的例子来说:假如不是a=1的操作,而是a=new byte[1024*1024](分配1m空间)`,那么它会运行地很慢,此时cpu是等待其执行结束呢,还是先执行下面那句flag=true呢?显然,先执行flag=true可以提前使用cpu,加快整体效率,当然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。

在硬件层面,cpu会将接收到的一批指令按照其规则重排序,同样是基于cpu速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从jvm并发看cpu内存指令重排序(memory reordering)》

重排序很不好理解,上面只是简单地提了下其场景,要想较好地理解这个概念,需要构造一些例子和图表,在这里介绍两篇介绍比较详细、生动的文章《happens-before俗解》和《深入理解java内存模型(二)——重排序》。其中的“as-if-serial”是应该掌握的,即:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、运行时和处理器都必须遵守“as-if-serial”语义。拿个简单例子来说,

public void execute(){
 int a=0;
 int b=1;
 int c=a+b;
}

这里a=0,b=1两句可以随便排序,不影响程序逻辑结果,但c=a+b这句必须在前两句的后面执行。

从前面那个例子可以看到,重排序在多线程环境下出现的概率还是挺高的,在关键字上有volatile和synchronized可以禁用重排序,除此之外还有一些规则,也正是这些规则,使得我们在平时的编程工作中没有感受到重排序的坏处。

程序次序规则(program order rule):在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是代码顺序,因为要考虑分支、循环等结构。

监视器锁定规则(monitor lock rule):一个unlock操作先行发生于后面对同一个对象锁的lock操作。这里强调的是同一个锁,而“后面”指的是时间上的先后顺序,如发生在其他线程中的lock操作。

volatile变量规则(volatile variable rule):对一个volatile变量的写操作发生于后面对这个变量的读操作,这里的“后面”也指的是时间上的先后顺序。

线程启动规则(thread start rule):thread独享的start()方法先行于此线程的每一个动作。

线程终止规则(thread termination rule):线程中的每个操作都先行发生于对此线程的终止检测,我们可以通过thread.join()方法结束、thread.isalive()的返回值检测到线程已经终止执行。

线程中断规则(thread interruption rule):对线程interrupte()方法的调用优先于被中断线程的代码检测到中断事件的发生,可以通过thread.interrupted()方法检测线程是否已中断。

对象终结原则(finalizer rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

传递性(transitivity):如果操作a先行发生于操作b,操作b先行发生于操作c,那就可以得出操作a先行发生于操作c的结论。

正是以上这些规则保障了happen-before的顺序,如果不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序,也就是“如果在本线程中观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,则不符合以上规则的都是无序的”,因此,如果我们的多线程程序依赖于代码书写顺序,那么就要考虑是否符合以上规则,如果不符合就要通过一些机制使其符合,最常用的就是synchronized、lock以及volatile修饰符。

以上这篇浅谈java指令重排序的问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。