`
春花秋月何时了
  • 浏览: 42456 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

Java内存模型JMM之二指令重排序及内存屏障

 
阅读更多

重排序背景

         现代计算机的处理器架构几乎都采用流水线机制在一颗处理器内核中实现指令级并行运算(甚至一个CPU拥有多条独立的流水线这称为超标量),简单地理解即是说,将一条指令的执行过程拆分成不同的执行周期(例如取指、译码、转址、执行、写回等)分散到若干级流水线上执行,从而达到多条指令能同时处于这条流水线上的不同级别,称为指令级并行。

         在这样的流水线模型中,当某一条指令在某级流水线处理时间太长就会阻塞上一级已经执行完并正在等待该级流水线执行的指令的执行,从而严重降低了处理性能,这中现象称之为流水线阻塞或者流水线气泡。为了避免这种现象,处理器在将指令推入流水线之前可以根据它们之间的数据依耐性进行重排序,从而消除这种阻塞,当然这已经是很早期的CPU流水线设计了,Intel在奔腾系列开始就已经在流水线模型中加入了乱序执行部件,只要当前微指令所需的数据就绪,而且存在空闲的执行单元,微指令就可以立即执行,甚至跳过前面还未就绪的微指令,然而就算有了这些激动人心的改进,CPU对指令的重排序依然是不可或缺的。

 

重排序的分类

一条程序的执行,为了提高性能,编译器和处理器事先一般都会对其指令进行重排序, 重排序分为以下三种类型:

  1. 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以依据上下文的分析重新安排语句的执行顺序。在Java中即是as-if-serial语义表达的含义:单线程环境中的操作均可以为了优化而被重排序,但是必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守as-if-serial语义。 
  2. 指令级并行重排序。在指令运行期,处理器在将指令提交至并行运算的流水线时,如果经过动态分析不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统重排序。要理解内存系统重排序,需要先了解缓存分片机制:我们知道为了提升CPU性能,CPU从直接存取主内存转移到各自的CPU缓存,但是CPU处理性能依然还是远远超过访问cache的速度,这种速度不匹配会造成cache wait,过多的cache wait会造成性能瓶颈。针对这种情况,多数架构采用了一种将cache分片的解决方案,即将一块cache划分成互不关联地多个slots(逻辑存储单元,又名Memory Bank或Cache Bank),CPU可以自行选择在多个idle bank中进行存取,从而能够显著提升指令并行处理能力。回到重排序问题上,在指令运行期间,如果指令1要操作的cache bank处于busy状态,而指令2要操作的cache bank处于idle状态,那么CPU为了防止cache 等待,可能会对着两个指令的内存访问操作进行重排序,即先执行后面的指令2。

重排序可能会导致这样的结果:

//代码顺序                        //执行顺序
int number= 1;                    int result = 0;
int result = 0;                   int number = 1;

 以上重排序满足as-if-serial语义,即程序执行的结果应该与代码执行的结果一致,所以重排序是被允许的。但是存在数据依赖的语句的执行不能被重排序,如下三种情况重排序将改变程序执行结果,所以不会被编译器以及处理器重排序:

 

写后读 a = 1;b = a; 写一个变量之后,再读这个位置
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量
读后写 a = b;b = 1; 读一个变量之后,再写这个变量

 

重排序对多线程的影响

public class RecordExample {
    int a = 0;
    boolean flag = false;

    /**
     * A线程执行
     */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }

    /**
     * B线程执行
     */
    public void read(){
        if(flag){                  // 3
           int i = a + a;          // 4
        }
    }

}

 A线程执行writer(),线程B执行read(),线程B在执行时能否读到 a = 1 呢?答案是不一定(当然和特定的CPU架构相关,如X86CPU不支持写重排序,那么结果就是确定的1)。

分析:由于操作1 和操作2 之间没有数据依赖性,所以可以进行重排序处理。

           由于操作3 和操作4 之间也没有数据依赖性,他们亦可以进行重排序。但是操作3和操作4之间存在控制依赖关系,当代码中存在控制依赖时,会影响指令序列的并行度,为此,编译器和处理器会采用 猜测执行来克服这种影响,即线程B可以提前读取并计算a + a,然后把结果临时保存到一个名为重排序缓冲的硬件缓存中,当操作3满足时,就把该结果写入变量i中。

通过上面的分析,重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

 

内存屏障 

通过前面的介绍,我们知道不但现代计算器都是多核CPU,而且每个CPU核心还有单独的缓存, 并且这些缓存并不是实时都与主存发生信息交换,因为与主存之间的交互操作需要很大的性能开销,CPU为了保证指令流水线持续运行,一般会以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一块内存地址的多次写,减少对内存总线的占用,这样就可能造成多个CPU上的缓存数据不一致从而使多线程的运行出现问题,另一方面,指令重排序的现象进一步打乱了指令的执行顺序,给其它CPU提供不可测的运行顺序。

内存屏障通过牺牲CPU的优化技术在一定程度上消除了这种想象,内存屏障(Memory Barrier),又称内存栅栏,是一个CPU指令。内存屏障有两个功能:

a)确保一些特定操作执行的顺序。通过内存屏障可以禁止特定类型处理器的重排序,确保从另一个CPU来看屏障的两边的所有指令都是正确的程序顺序,而保持程序顺序的外部可见性。

b)影响一些数据的可见性。通过内存屏障可以强制把写缓冲区/高速缓存中的数据立即写回主内存,让其它CPU的缓存中相应的数据失效。

 

大多数的内存屏障都是复杂的话题。在不同的CPU架构上内存屏障的实现非常不一样,Java内存模型屏蔽了这种底层硬件平台的差异。为了简化并方便理解,仅以X86架构来阐述。x86主要有以下几种内存屏障:

1. Store屏障,是x86上的”sfence“指令,强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区的数据都写会主内存。这会使得程序状态对其它CPU可见。通俗地讲就是:在写指令之后插入写屏障,能让写入缓存的最新数据立即写回到主内存。

2. Load屏障,是x86上的”ifence“指令,强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令。通俗地讲就是:在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。

3. Full屏障,是x86上的”mfence“指令,是一种全能型的屏障,复合了Load和Store屏障的功能,所以开销也是比较大的。

 

Lock前缀,Lock不是一种内存屏障,但是它能完成类似Full内存屏障的功能。Lock会对CPU总线或者高速缓存加锁,可以理解为CPU指令级的一种锁。

1. 它先对总线/缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据全部刷新回主内存。

2. Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。Lock后的写操作会让其他CPU相关的cache line失效,从而使其重新从内存加载最新的数据。这个是通过缓存一致性协议做的。

  • 大小: 59.2 KB
分享到:
评论

相关推荐

    深入Java内存模型-JMM

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何保证各个线程对共享数据的一致性视图。JMM的主要目标是定义程序中各个变量的访问规则,以及在...

    深入理解Java内存模型

    Java内存模型中的重排序是一个重要组成部分,它分为编译器重排序、指令级并行的重排序以及内存系统的重排序。编译器优化的重排序是为了提高单线程程序的性能,而指令级并行的重排序和内存系统的重排序则是在多线程...

    java内存模型JMM(Java Memory Model)1

    4. **有序性**:JMM允许编译器和处理器对指令进行重排序,但为了保持线程安全,它规定了特定的内存屏障(内存序)来限制这种重排序。例如,`synchronized`关键字可以保证代码块的执行顺序,避免指令重排序带来的问题...

    深入理解Java内存模型 pdf 超清版

    Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了程序中各个线程如何访问和修改共享变量,以及如何确保数据的一致性。深入理解Java内存模型对于编写高效的并发程序至关重要。本文...

    深入理解Java内存模型.程晓明(带书签文字版).pdf

    处理器重排序与内存屏障指令 7 happens-before 10 重排序 13 数据依赖性 13 as-if-serial 语义 13 程序顺序规则 15 重排序对多线程的影响 15 顺序一致性 19 数据竞争与顺序一致性保证 19 顺序一致性内存模型...

    深入理解 Java 内存模型

    Java 内存模型(Java Memory Model,简称 JMM)是 Java 平台中关于线程如何访问共享变量的一套规则,它定义了线程之间的内存可见性、数据一致性以及指令重排序等关键概念,对于多线程编程和并发性能优化至关重要。...

    深入理解 Java 内存模型_程晓明_InfoQ_java_内存模型_

    Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了线程如何共享和访问内存,以及在多线程环境中如何保证数据一致性。理解JMM对于编写高效、正确且线程安全的Java代码至关重要。 ...

    深入理解java内存模型

    Java内存模型(Java Memory Model,JMM)是Java平台中非常关键的概念,它定义了线程如何共享和访问内存中的数据,以及在多线程环境下如何保证数据的一致性。这本书"深入理解Java内存模型"显然是为了帮助读者深入探讨...

    java内存模型文档

    Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了线程如何共享和访问内存,以及在并发编程中如何处理数据一致性的问题。理解JMM对于编写高效、线程安全的Java代码至关重要。 1. ...

    三问JMM--有关JVM内存模型的PPT

    Java通过内存屏障技术来保证有序性,防止编译器或处理器对内存访问指令进行重排序。 综上所述,JMM通过定义一套规则来管理多线程环境下的内存一致性问题,确保程序能够在并发环境中正确地执行。理解JMM的核心概念...

    深入理解Java内存模型(经典).rar

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何保证共享数据的正确性。深入理解JMM对于编写高效、线程安全的Java代码至关重要。 首先,我们要...

    深入理解Java内存模型(二)共3页.pdf.zip

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何在共享内存中读写变量,以及这些读写操作的可见性、原子性和有序性。这个模型规定了线程与主内存...

    Java内存模型1

    为了解决这个问题,JMM引入了内存屏障,这是一种硬件指令,用于确保特定内存操作的顺序,防止指令重排序带来的不确定性。 在处理器层面,内存模型有两个核心要求:一是当前处理器能够看到其他处理器写入内存的数据...

    java内存模型

    为了实现这一目标,JMM会插入内存屏障指令来禁止特定类型的重排序,确保程序在不同的编译器和处理器平台上行为一致。 重排序分为编译器重排序、指令级并行的重排序和内存系统的重排序。编译器可能会为了提高性能...

    java内存模型和一些多线程的资料

    JMM通过内存屏障(内存栅栏)来限制指令重排序,确保在特定条件下维持程序执行的顺序性。 4. **原子性** 原子操作是指不可中断的一个或一系列操作。在Java中,synchronized和volatile关键字可以帮助保证某些操作的...

    JAVA内存模型.docx

    Java 内存模型(JMM)是Java编程中不可或缺的一部分,它定义了Java虚拟机(JVM)如何处理多线程环境下的内存访问。在Java并发编程中,理解和掌握JMM至关重要,因为它确保了共享变量在多线程间的正确同步和一致性。 ...

Global site tag (gtag.js) - Google Analytics