指令重排序

对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(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. ApplicationContext.xml文件详解

    想必用过Spring的程序员们都有这样的感觉,Spring把逻辑层封装的太完美了(个人感觉View层封装的不是很好).以至于有的初学者都不知道Spring配置文件的意思,就拿来用了.所以今天我给大家详 ...

  2. [MVCSharp]开始使用MVC#

    Getting started with MVC# framework The source code of this example can be found under "Example ...

  3. jquery .on的使用

    1.7版本以上,开始使用.on绑定时间 给jquery动态产生的元素绑定事件不能使用普通的$("#fff").click(function(){alert("ok&quo ...

  4. [转]【android studio】解决layout预览出现Rendering Problems Exception Unable to find the layout for Action Bar.

    在android studio中打开layout文件,发现不能预览布局,提示以下错误: Rendering Problems Exception raised during rendering: Un ...

  5. spring4 文件下载功能

    需要准备的工具和框架 Spring 4.2.0.RELEASE Bootstrap v3.3.2 Maven 3 JDK 1.7 Tomcat 8.0.21 Eclipse JUNO Service ...

  6. bind: address already in use

    2016/04/18 09:46:06 server.go:36: listen at 0.0.0.0:9530 2016/04/18 09:46:06 server.go:39: listen er ...

  7. weka特征选择(IG、chi-square)

    一.说明 IG是information gain 的缩写,中文名称是信息增益,是选择特征的一个很有效的方法(特别是在使用svm分类时).这里不做详细介绍,有兴趣的可以googling一下. chi-s ...

  8. Topcoder SRM 597

    妈蛋第一场tc就掉分,提交了第一个题的时候就注定悲剧要发生了,妈蛋没考虑0就直接%了,真的是人傻见识又少,第二题最后有了一点思路,没时间写了,可能也不是很准确,第三题想了小会儿效果为0! 然后第一题傻 ...

  9. Web Project配置Hirbernate

    1:首先找到hibernate-release-4.1.9.Final.zip\hibernate-release-4.1.9.Final\lib\required ,把required里的所有jar ...

  10. linux env

    .Linux的变量种类 按变量的生存周期来划分,Linux变量可分为两类: 1.1 永久的:需要修改配置文件,变量永久生效. 1.2 临时的:使用export命令声明即可,变量在关闭shell时失效. ...