指令重排序

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

转载于:https://www.cnblogs.com/dreamroute/p/4078226.html

指令重排序及Happens-before法则随笔相关推荐

  1. 【Java 并发编程】指令重排序规范 ( happens-before 先行发生原则 )

    文章目录 一.指令重排序规范 二.happens-before 先行发生原则 一.指令重排序规范 指令重排指的是 , 线程中如果两行代码 没有逻辑上的上下关系 , 可以对代码进行 重新排序 ; JVM ...

  2. 【Java 并发编程】线程指令重排序问题 ( 指令重排序规范 | volatile 关键字禁止指令重排序 )

    文章目录 总结 一.指令重排序规范 二.指令重排序示例 总结 Java 并发的 333 特性 : 原子性 : 每个操作都是 不可拆分的原子操作 ; 在线程中进行 a++ 就不是原子操作 , 该操作分为 ...

  3. 说说Java中原子性,可见性与指令重排序的理解

    原子性:就是读数据,处理数据,写数据 这三个步骤不能被终止,或者打断:就是不能被线程调度器中断,切换线程. 这样,才能保证,原子操作在线程切换,并行处理上保证数据地顺序累加处理. 可见性:是Jvm较为 ...

  4. Java之volatile如何保证可见性和指令重排序

    1 我们先了解CPU缓存 CPU缓存为了解决CPU运算速度与内存读写速度不匹配的问题,因为CPU运算速度要比内存读写速度快得多 一次主内存的访问通常在几十到几百个时钟周期 一次L1高速缓存的读写只需要 ...

  5. java volidate线程安全_03.(多线程与并发)面试题-02--Volidate的原理和指令重排序

    线程栈(线程的工作内存)保存了线程运行时候变量值信息.当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本 ...

  6. JVM学习--(二)内存模型、可见性、指令重排序

    我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存模型 首先我们思考一下一个java线程要向另外一个线程进行通信,应该怎么做,我们再 ...

  7. 指令重排序所带来的问题及使用volatile关键字解决问题

    首先看下如下代码: 指令重排序和优化后代码如下: if(!stop) while(true){} volatile最适合使用的是一个线程写.其他线程读的场合,如果有多个线程并发写操作,仍然需要使用锁或 ...

  8. 【java】java如何证明java发生了指令重排序

    1.概述 package com.java.memory.commondsort;/*** @author: chuanchuan.lcc* @date: 2020-12-20 19:37* @mod ...

  9. java重排序_Java synchronized 能防止指令重排序吗?

    @ZealTalk 说的是 synchronized 可以防止指令重排,这个观点不对的,也欢迎回答的各位来讨论 synchronized 的有序性 来讨论这个问题先,先看看 Java 里的操作无序现象 ...

最新文章

  1. 自定义控件--基础2
  2. 给一个词就能模仿你的笔迹,Facebook这个AI强大到不敢开源代码
  3. ajax文字上下滚动,ajax页面底部 滚动加载效果 实例
  4. rest-framework:频率控制
  5. Linux 操作命令(1)
  6. bq4050读固件_stm32f767 实现模拟SMBUS驱动bq4050
  7. slqite3库查询数据处理方式_SQLite3命令操作大全
  8. charles 中文_抓包工具--charles(青花瓷)及获取AppStore数据包
  9. Ubuntu16使用theano出错
  10. BZOJ 1597 [Usaco2008 Mar] 土地购买
  11. 再谈Redis应用场景
  12. 墨刀产品设计大赛获奖名单出炉了!作品都太强了,速来围观
  13. 【EXLIBRIS】纸版书目整理 -- 增补至2008年春节【36 种】【36 册】
  14. Vue传递参数不在URL路径上 显示参数的完整方法
  15. 创业都是从一个想法开始
  16. 做好cpc广告的五部攻略 你就能日出百单
  17. ​宝尊电商年亏2亿:阿里又成大股东 软银大幅减持
  18. amd兼容AM3服务器cpu型号,兼容性超强的AM2/AM2+/AM3接口_主板新闻-中关村在线
  19. Code Composer Studio 安装教程与入门
  20. python实现ping工具

热门文章

  1. linux内存一直占满问题
  2. Docker入门与实战
  3. Java高并发编程详解系列-线程异常处理
  4. MongoDB中的索引操作
  5. mybatis的mapper接口与xml传参问题
  6. Nginx应用场景之静态服务器
  7. 关于spring cloud的几个问题
  8. bzoj 1597 [Usaco2008 Mar]土地购买——斜率优化dp
  9. 跟我一起学习ASP.NET 4.5 MVC4.0(一)
  10. Oracle HA 之 OGG部署流水