指令重排序

对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。

重排序的背景

我们知道现代CPU的主频越来越高,与cache的交互次数也越来越多。当CPU的计算速度远远超过访问cache时,会产生cache wait,过多的cache  wait就会造成性能瓶颈。
针对这种情况,多数架构(包括X86)采用了一种将cache分片的解决方案,即将一块cache划分成互不关联地多个 slots (逻辑存储单元,又名 Memory Bank 或 Cache Bank),CPU可以自行选择在多个 idle bank 中进行存取。这种 SMP 的设计,显著提高了CPU的并行处理能力,也回避了cache访问瓶颈。

Memory Bank的划分
一般 Memory bank 是按cache address来划分的。比如 偶数adress 0×12345000 分到 bank 0, 奇数address 0×12345100 分到 bank1。

重排序的种类
编译期重排。编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。

运行期重排,CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化。

 

Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。

程序执行最简单的模型是按照指令出现的顺序执行,这样就与执行指令的CPU无关,最大限度的保证了指令的可移植性。这个模型的专业术语叫做顺序化一致性模型。但是现代计算机体系和处理器架构都不保证这一点(因为人为的指定并不能总是保证符合CPU处理的特性)。

我们来看最经典的一个案例。

package xylz.study.concurrency.atomic; 

public class ReorderingDemo { 

    static int x = 0, y = 0, a = 0, b = 0; 

    public static void main(String[] args) throws Exception { 

        for (int i = 0; i < 100; i++) {
            x=y=a=b=0;
            Thread one = new Thread() {
                public void run() {
                    a = 1;
                    x = b;
                }
            };
            Thread two = new Thread() {
                public void run() {
                    b = 1;
                    y = a;
                }
            };
            one.start();
            two.start();
            one.join();
            two.join();
            System.out.println(x + " " + y);
        }
    } 

}

在这个例子中one/two两个线程修改区x,y,a,b四个变量,在执行100次的情况下,可能得到(0 1)或者(1 0)或者(1 1)。事实上按照JVM的规范以及CPU的特性有很可能得到(0 0)。当然上面的代码大家不一定能得到(0 0),因为run()里面的操作过于简单,可能比启动一个线程花费的时间还少,因此上面的例子难以出现(0,0)。但是在现代CPU和JVM上确实是存在的。由于run()里面的动作对于结果是无关的,因此里面的指令可能发生指令重排序,即使是按照程序的顺序执行,数据变化刷新到主存也是需要时间的。假定是按照a=1;x=b;b=1;y=a;执行的,x=0是比较正常的,虽然a=1在y=a之前执行的,但是由于线程one执行a=1完成后还没有来得及将数据1写回主存(这时候数据是在线程one的堆栈里面的),线程two从主存中拿到的数据a可能仍然是0(显然是一个过期数据,但是是有可能的),这样就发生了数据错误。

在两个线程交替执行的情况下数据的结果就不确定了,在机器压力大,多核CPU并发执行的情况下,数据的结果就更加不确定了。

 

Happens-before法则

Java的内存结构如下

如果多线程之间不共享数据,这也表现得很好,但是如果多线程之间要共享数据,那么这些乱序执行,数据在寄存器中这些行为将导致程序行为的不确定性,现在处理器已经是多核时代了,这些问题将会更加严重,每个线程都有自己的工作内存,多个线程共享主内存,如图

如果共享数据,什么时候同步到主内存让别人的线程读取数据呢?这又是不确定的,如果非要一致,那么代价高昂,这将牺牲处理器的性能,所以现在的处理器会牺牲存储一致性来换取性能,如果程序要确保共享数据的时候获得一致性,处理器通常了提供了一些关卡指令,这个可以帮助程序员来实现,但是各种处理器都不一样,如果要使程序能够跨平台是不可能的,怎么办?

使用Java,由JMM(Java Memeory Model Action)来屏蔽,我们只要和JMM的规定来使用一致性保证就搞定了,那么JMM又提供了什么保证呢?JMM的定义是通过动作的形式来描述的,所谓动作,包括变量的读和写,监视器加锁和释放锁,线程的启动和拼接,这就是传说中的happen before,要想A动作看到B动作的结果,B和A必须满足happen before关系,happen before法则如下:

1, 程序次序法则,如果A一定在B之前发生,则happen before,

2, 监视器法则,对一个监视器的解锁一定发生在后续对同一监视器加锁之前

3, Volatie变量法则:写volatile变量一定发生在后续对它的读之前

4, 线程启动法则:Thread.start一定发生在线程中的动作

5, 线程终结法则:线程中的任何动作一定发生在括号中的动作之前(其他线程检测到这个线程已经终止,从Thread.join调用成功返回,Thread.isAlive()返回false)

6, 中断法则:一个线程调用另一个线程的interrupt一定发生在另一线程发现中断。

7, 终结法则:一个对象的构造函数结束一定发生在对象的finalizer之前

8, 传递性:A发生在B之前,B发生在C之前,A一定发生在C之前。

转自:http://blog.163.com/javaee_chen/blog/static/179195077201131382128499/

指令重排序及Happens-before法则随笔的更多相关文章

  1. 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则

    转: http://www.blogjava.net/xylz/archive/2010/07/03/325168.html 在这个小结里面重点讨论原子操作的原理和设计思想. 由于在下一个章节中会谈到 ...

  2. 深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则[转]

    在这个小结里面重点讨论原子操作的原理和设计思想. 由于在下一个章节中会谈到锁机制,因此此小节中会适当引入锁的概念. 在Java Concurrency in Practice中是这样定义线程安全的: ...

  3. 深入浅出Java并发包—指令重排序

    前面大致提到了JDK中的一些个原子类,也提到原子类是并发的基础,更提到所谓的线程安全,其实这些类或者并发包中的这么一些类,都是为了保证系统在运行时是线程安全的,那到底怎么样才算是线程安全呢? Java ...

  4. Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  5. Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  6. JVM并发机制的探讨——内存模型、内存可见性和指令重排序

    并发本来就是个有意思的问题,尤其是现在又流行这么一句话:“高帅富加机器,穷矮搓搞优化”. 从这句话可以看到,无论是高帅富还是穷矮搓都需要深入理解并发编程,高帅富加多了机器,需要协调多台机器或者多个CP ...

  7. 关于volatile的可见性和禁止指令重排序的疑惑

    在学习volatile语义的可见性和禁止指令重排序的相关测试中,发现并不能体现出禁止指令重排序的特性 实验代码如下 package com.aaron.beginner.multithread.vol ...

  8. 轻松学JVM(二)——内存模型、可见性、指令重排序

    上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...

  9. java指令重排序的问题

    转载自于:http://my.oschina.net/004/blog/222069?fromerr=ER2mp62C 指令重排序是个比较复杂.觉得有些不可思议的问题,同样是先以例子开头(建议大家跑下 ...

随机推荐

  1. Linux就这个范儿 第18章 这里也是鼓乐笙箫 Linux读写内存数据的三种方式

    Linux就这个范儿 第18章  这里也是鼓乐笙箫  Linux读写内存数据的三种方式 P703 Linux读写内存数据的三种方式 1.read  ,write方式会在用户空间和内核空间不断拷贝数据, ...

  2. 项目重新部署后报The attribute required is undefined for the annotation type XmlElementRef

    在另外一台机器上部署项目,项目导进Eclipse中发现有异常 public class BooleanFeatureType extends FeatureBaseType{ @XmlElementR ...

  3. ZOOKEEPER解惑

    现在网上关于ZooKeeper的文章很多,有介绍Leader选举算法的,有介绍ZooKeeper Server内部原理的,还有介绍ZooKeeper Client的.本文不打算再写类似的内容,而专注与 ...

  4. jQuery 插件 获取URL参数

    jQuery 获取URL参数的插件 jQuery Url Query String 下载地址:http://plugins.jquery.com/getUrlQueryString.js/   var ...

  5. 四种DLL:NON-MFC DLL, Regular DLL Statically/Dynamically Linked to MFC, MFC Extension DLL

    参考资料: https://msdn.microsoft.com/en-us/library/30c674tx.aspx http://www.cnblogs.com/qrlozte/p/484442 ...

  6. new操作符做了什么??

    在javascript中,new操作符随处可见,我讲一下我自己对new操作符的理解... 构造函数无返回值 //测试代码 function Foo(name) { var age = 20; this ...

  7. itunes connect

    https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/iTunesConnec ...

  8. Java定时任务Timer、TimerTask与ScheduledThreadPoolExecutor详解

     定时任务就是在指定时间执行程序,或周期性执行计划任务.Java中实现定时任务的方法有很多,本文从从JDK自带的一些方法来实现定时任务的需求. 一.Timer和TimerTask  Timer和Tim ...

  9. tooltip

    /* 背景色 ; 字体颜色 ; 云,显示在上面 */ .tooltip-inner{ background-color: #FF0000; ForeColor:#0f0; IsBalloon:true ...

  10. NGINX反向代理

                                                    Nginx反向代理                                           ...