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)应该永远不会为
 * 真,永远不会打印.
 * 但实际状况是:在试验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修饰符。多线程

相关文章
相关标签/搜索