精华内容
下载资源
问答
  • 指令重排对于编译器的编写者来说,Java内存模型(JMM)主要是由禁止指令重排的规则所组成的,其中包括了字段(包括数组中的元素)的存取指令和监视器(锁)的控制指令。Volatile监视器JMM中关于volatile和监视器主要的...

    指令重排

    对于编译器的编写者来说,Java内存模型(JMM)主要是由禁止指令重排的规则所组成的,其中包括了字段(包括数组中的元素)的存取指令和监视器(锁)的控制指令。

    Volatile与监视器

    JMM中关于volatile和监视器主要的规则可以被看作一个矩阵。这个矩阵的单元格表示在一些特定的后续关联指令的情况下,指令不能被重排。下面的表格并不是JMM规范包含的,而是一个用来观察JMM模型对编译器和运行系统造成的主要影响的工具。

    能否重排

    第二个操作

    第一个操作

    Normal Load Normal Store

    Volatile load MonitorEnter

    Volatile store MonitorExit

    Normal Load Normal Store

    No

    Volatile load MonitorEnter

    No

    No

    No

    Volatile store MonitorExit

    No

    No

    关于上面这个表格一些术语的说明:

    Normal Load指令包括:对非volatile字段的读取,getfield,getstatic和array load;

    Normal Store指令包括:对非volatile字段的存储,putfield,putstatic和array store;

    Volatile load指令包括:对多线程环境的volatile变量的读取,getfield,getstatic;

    Volatile store指令包括:对多线程环境的volatile变量的存储,putfield,putstatic;

    MonitorEnters指令(包括进入同步块synchronized方法)是用于多线程环境的锁对象;

    MonitorExits指令(包括离开同步块synchronized方法)是用于多线程环境的锁对象。

    在JMM中,Normal Load指令与Normal store指令的规则是一致的,类似的还有Volatile load指令与MonitorEnter指令,以及Volatile store指令与MonitorExit指令,因此这几对指令的单元格在上面表格里都合并在了一起(但是在后面部分的表格中,会在有需要的时候展开)。在这个小节中,我们仅仅考虑那些被当作原子单元的可读可写的变量,也就是说那些没有位域(bit fields),非对齐访问(unaligned accesses)或者超过平台最大字长(word size)的访问。

    任意数量的指令操作都可被表示成这个表格中的第一个操作或者第二个操作。例如在单元格[Normal Store, Volatile Store]中,有一个No,就表示任何非volatile字段的store指令操作不能与后面任何一个Volatile store指令重排, 如果出现任何这样的重排会使多线程程序的运行发生变化。

    JSR-133规范规定上述关于volatile和监视器的规则仅仅适用于可能会被多线程访问的变量或对象。因此,如果一个编译器可以最终证明(往往是需要很大的努力)一个锁只被单线程访问,那么这个锁就可以被去除。与之类似的,一个volatile变量只被单线程访问也可以被当作是普通的变量。还有进一步更细粒度的分析与优化,例如:那些被证明在一段时间内对多线程不可访问的字段。

    在上表中,空白的单元格代表在不违反Java的基本语义下的重排是允许的(详细可参考JLS中的说明)。例如,即使上表中没有说明,但是也不能对同一个内存地址上的load指令和之后紧跟着的store指令进行重排。但是你可以对两个不同的内存地址上的load和store指令进行重排,而且往往在很多编译器转换和优化中会这么做。这其中就包括了一些往往不认为是指令重排的例子,例如:重用一个基于已经加载的字段的计算后的值而不是像一次指令重排那样去重新加载并且重新计算。然而,JMM规范允许编译器经过一些转换后消除这些可以避免的依赖,使其可以支持指令重排。

    在任何的情况下,即使是程序员错误的使用了同步读取,指令重排的结果也必须达到最基本的Java安全要求。所有的显式字段都必须不是被设定成0或null这样的预构造值,就是被其他线程设值。这通常必须把所有存储在堆内存里的对象在其被构造函数使用前进行归零操作,并且从来不对归零store指令进行重排。一种比较好的方式是在垃圾回收中对回收的内存进行归零操作。可以参考JSR-133规范中其他情况下的一些关于安全保证的规则。

    这里描述的规则和属性都是适用于读取Java环境中的字段。在实际的应用中,这些都可能会另外与读取内部的一些记账字段和数据交互,例如对象头,GC表和动态生成的代码。

    Final 字段

    Final字段的load和store指令相对于有锁的或者volatile字段来说,就跟Normal load和Normal store的存取是一样的,但是需要加入两条附加的指令重排规则:

    如果在构造函数中有一条final字段的store指令,同时这个字段是一个引用,那么它将不能与构造函数外后续可以让持有这个final字段的对象被其他线程访问的指令重排。例如:你不能重排下列语句:

    x.finalField = v;

    ... ;

    sharedRef = x;

    这条规则会在下列情况下生效,例如当你内联一个构造函数时,正如“…”的部分表示这个构造函数的逻辑边界那样。你不能把这个构造函数中的对于这个final字段的store指令移动到构造函数外的一条store指令后面,因为这可能会使这个对象对其他线程可见。(正如你将在下面看到的,这样的操作可能还需要声明一个内存屏障)。类似的,你不能把下面的前两条指令与第三条指令进行重排:

    x.afield = 1; x.finalField = v; ... ; sharedRef = x;

    一个final字段的初始化load指令不能与包含该字段的对象的初始化load指令进行重排。在下面这种情况下,这条规则就会生效:x = shareRef; … ; i = x.finalField;

    由于这两条指令是依赖的,编译器将不会对这样的指令进行重排。但是,这条规则会对某些处理器有影响。

    上述规则,要求对于带有final字段的对象的load本身是synchronized,volatile,final或者来自类似的load指令,从而确保java程序员对与final字段的正确使用,并最终使构造函数中初始化的store指令和构造函数外的store指令排序。

    内存屏障

    编译器和处理器必须同时遵守重排规则。由于单核处理器能确保与“顺序执行”相同的一致性,所以在单核处理器上并不需要专门做什么处理,就可以保证正确的执行顺序。但在多核处理器上通常需要使用内存屏障指令来确保这种一致性。即使编译器优化掉了一个字段访问(例如,因为一个读入的值未被使用),这种情况下还是需要产生内存屏障,就好像这个访问仍然需要保护。(可以参考下面的优化掉内存屏障的章节)。

    内存屏障仅仅与内存模型中“获取”、“释放”这些高层次概念有间接的关系。内存屏障并不是“同步屏障”,内存屏障也与在一些垃圾回收机制中“写屏障(write barriers)”的概念无关。内存屏障指令仅仅直接控制CPU与其缓存之间,CPU与其准备将数据写入主存或者写入等待读取、预测指令执行的缓冲中的写缓冲之间的相互操作。这些操作可能导致缓冲、主内存和其他处理器做进一步的交互。但在JAVA内存模型规范中,没有强制处理器之间的交互方式,只要数据最终变为全局可用,就是说在所有处理器中可见,并当这些数据可见时可以获取它们。

    内存屏障的种类

    几乎所有的处理器至少支持一种粗粒度的屏障指令,通常被称为“栅栏(Fence)”,它保证在栅栏前初始化的load和store指令,能够严格有序的在栅栏后的load和store指令之前执行。无论在何种处理器上,这几乎都是最耗时的操作之一(与原子指令差不多,甚至更消耗资源),所以大部分处理器支持更细粒度的屏障指令。

    内存屏障的一个特性是将它们运用于内存之间的访问。尽管在一些处理器上有一些名为屏障的指令,但是正确的/最好的屏障使用取决于内存访问的类型。下面是一些屏障指令的通常分类,正好它们可以对应上常用处理器上的特定指令(有时这些指令不会导致操作)。

    LoadLoad 屏障

    序列:Load1,Loadload,Load2

    确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。

    StoreStore 屏障

    序列:Store1,StoreStore,Store2

    确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。

    LoadStore 屏障

    序列: Load1; LoadStore; Store2

    确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。

    StoreLoad Barriers

    序列: Store1; StoreLoad; Load2

    确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。

    在下面讨论的所有处理器中,执行StoreLoad的指令也会同时获得其他三种屏障的效果。所以StoreLoad可以作为最通用的(但通常也是最耗性能)的一种Fence。(这是经验得出的结论,并不是必然)。反之不成立,为了达到StoreLoad的效果而组合使用其他屏障并不常见。

    下表显示这些屏障如何符合JSR-133排序规则。

    需要的屏障

    第二步

    第一步

    Normal Load

    Normal Store

    Volatile Load MonitorEnter

    Volatile Store MonitorExit

    Normal Load

    LoadStore

    Normal Store

    StoreStore

    Volatile Load MonitorEnter

    LoadLoad

    LoadStore

    LoadLoad

    LoadStore

    Volatile Store MonitorExit

    StoreLoad

    StoreStore

    另外,特殊的final字段规则在下列代码中需要一个StoreStore屏障

    x.finalField = v; StoreStore; sharedRef = x;

    如下例子解释如何放置屏障:

    class X {

    int a, b;

    volatile int v, u;

    void f() {

    int i, j;

    i = a;// load a

    j = b;// load b

    i = v;// load v

    // LoadLoad

    j = u;// load u

    // LoadStore

    a = i;// store a

    b = j;// store b

    // StoreStore

    v = i;// store v

    // StoreStore

    u = j;// store u

    // StoreLoad

    i = u;// load u

    // LoadLoad

    // LoadStore

    j = b;// load b

    a = i;// store a

    }

    }

    数据依赖和屏障

    一些处理器为了保证依赖指令的交互次序需要使用LoadLoad和LoadStore屏障。在一些(大部分)处理器中,一个load指令或者一个依赖于之前加载值的store指令被处理器排序,并不需要一个显式的屏障。这通常发生于两种情况,间接取值(indirection):

    Load x; Load x.field

    和条件控制(control)

    Load x; if (predicate(x)) Load or Store y;

    但特别的是不遵循间接排序的处理器,需要为final字段设置屏障,使它能通过共享引用访问最初的引用。

    x = sharedRef; … ; LoadLoad; i = x.finalField;

    相反的,如下讨论,确定遵循数据依赖的处理器,提供了几个优化掉LoadLoad和LoadStore屏障指令的机会。(尽管如此,在任何处理器上,对于StoreLoad屏障不会自动清除依赖关系)。

    与原子指令交互

    屏障在不同处理器上还需要与MonitorEnter和MonitorExit实现交互。锁或者解锁通常必须使用原子条件更新操作CompareAndSwap(CAS)指令或者LoadLinked/StoreConditional (LL/SC),就如执行一个volatile store之后紧跟volatile load的语义一样。CAS或者LL/SC能够满足最小功能,一些处理器还提供其他的原子操作(如,一个无条件交换),这在某些时候它可以替代或者与原子条件更新操作结合使用。

    在所有处理器中,原子操作可以避免在正被读取/更新的内存位置进行写后读(read-after-write)。(否则标准的循环直到成功的结构体(loop-until-success )没有办法正常工作)。但处理器在是否为原子操作提供比隐式的StoreLoad更一般的屏障特性上表现不同。一些处理器上这些指令可以为MonitorEnter/Exit原生的生成屏障;其它的处理器中一部分或者全部屏障必须显式的指定。

    为了分清这些影响,我们必须把Volatiles和Monitors分开:

    需要的屏障

    第二步

    第一步

    Normal Load

    Normal Store

    Volatile Load

    Volatile Store

    MonitorEnter

    MonitorExit

    Normal Load

    LoadStore

    LoadStore

    Normal Store

    StoreStore

    StoreExit

    Volatile Load

    LoadLoad

    LoadStore

    LoadLoad

    LoadStore

    LoadEnter

    LoadExit

    Volatile Store

    StoreLoad

    StoreStore

    StoreEnter

    StoreExit

    MonitorEnter

    EnterLoad

    EnterStore

    EnterLoad

    EnterStore

    EnterEnter

    EnterExit

    MonitorExit

    ExitLoad

    ExitStore

    ExitEnter

    ExitExit

    另外,特殊的final字段规则需要一个StoreLoad屏障。

    x.finalField = v; StoreStore; sharedRef = x;

    在这张表里,”Enter”与”Load”相同,”Exit”与”Store”相同,除非被原子指令的使用和特性覆盖。特别是:

    EnterLoad 在进入任何需要执行Load指令的同步块/方法时都需要。这与LoadLoad相同,除非在MonitorEnter时候使用了原子指令并且它本身提供一个至少有LoadLoad属性的屏障,如果是这种情况,相当于没有操作。

    StoreExit在退出任何执行store指令的同步方法块时候都需要。这与StoreStore一致,除非MonitorExit使用原子操作,并且提供了一个至少有StoreStore属性的屏障,如果是这种情况,相当于没有操作。

    ExitEnter和StoreLoad一样,除非MonitorExit使用了原子指令,并且/或者MonitorEnter至少提供一种屏障,该屏障具有StoreLoad的属性,如果是这种情况,相当于没有操作。

    在编译时不起作用或者导致处理器上不产生操作的指令比较特殊。例如,当没有交替的load和store指令时,EnterEnter用于分离嵌套的MonitorEnter。下面这个例子说明如何使用这些指令类型:

    class X {

    int a;

    volatile int v;

    void f() {

    int i;

    synchronized (this) { // enter EnterLoad EnterStore

    i = a;// load a

    a = i;// store a

    }// LoadExit StoreExit exit ExitEnter

    synchronized (this) {// enter ExitEnter

    synchronized (this) {// enter

    }// EnterExit exit

    }// ExitExit exit ExitEnter ExitLoad

    i = v;// load v

    synchronized (this) {// LoadEnter enter

    } // exit ExitEnter ExitStore

    v = i; // store v

    synchronized (this) { // StoreEnter enter

    } // EnterExit exit

    }

    }

    Java层次的对原子条件更新的操作将在JDK1.5中发布(JSR-166),因此编译器需要发布相应的代码,综合使用上表中使用MonitorEnter和MonitorExit的方式,——从语义上说,有时在实践中,这些Java中的原子更新操作,就如同他们都被锁所包围一样。

    展开全文
  • Xcode 工程中嵌入汇编代码iOS 汇编入门教程(三)汇编中的 Section 数据存取iOS 汇编教程(四)基于 LLDB 动态调试快速分析系统函数的实现iOS 汇编教程(五)Objc Block 的内存布局和汇编表示前言具有 ARM 体系结构的...

    系列文章

    1. iOS 汇编入门教程(一)ARM64 汇编基础

    2. iOS 汇编入门教程(二)在 Xcode 工程中嵌入汇编代码

    3. iOS 汇编入门教程(三)汇编中的 Section 与数据存取

    4. iOS 汇编教程(四)基于 LLDB 动态调试快速分析系统函数的实现

    5. iOS 汇编教程(五)Objc Block 的内存布局和汇编表示

    前言

    具有 ARM 体系结构的机器拥有相对较弱的内存模型,这类 CPU 在读写指令重排序方面具有相当大的自由度,为了保证特定的执行顺序来获得确定结果,开发者需要在代码中插入合适的内存屏障,以防止指令重排序影响代码逻辑[1]。

    本文会介绍 CPU 指令重排的意义和副作用,并通过一个实验验证指令重排对代码逻辑的影响,随后介绍基于内存屏障的解决方案,以及在 iOS 开发中有关指令重排的注意事项。

    指令重排

    简介

    以 ARM 为体系结构的 CPU 在执行指令时,在遇到写操作时,如果未获得缓存段的独占权限,需要基于缓存一致性协议与其他核协商,等待直到获得独占权限时才能完成这条指令的执行;再或者在执行乘法指令时遇到乘法器繁忙的情况,也需要等待。在这些情况下,为了提升程序的执行速度,CPU 会优先执行一些没有前序依赖的指令。

    一个例子

    看下面一段简单的程序:

    ; void acc(int *counter, int *flag);_acc:ldr x8, [x0]add x8, x8, #1str x8, [x0]ldr x9, [x1]mov x9, #1str x9, [x1]ret

    这段代码将 counter 的值 +1,并将 flag 置为 1,按照正常的代码逻辑,CPU 先从内存中读取 counter (x0) 的值累加后回写,随后读取 flag (x1) 的值置位后回写。

    但是如果 x0 所在的内存未命中缓存,会带来缓存载入的等待,再或者回写时无法获取到缓存段的独占权,为了保证多核的缓存一致性,也需要等待;此时如果 x1 对应的内存有缓存段,则可以优先执行 ldr x9, [x1],同时由于对 x9 的操作和对 x1 所在内存的操作不依赖于对 x8 和 x0 所在内存的操作,后续指令也可以优先执行,因此 CPU 乱序执行的顺序可能变成如下这样:

    ldr x9, [x1]mov x9, #1str x9, [x1]ldr x8, [x0]add x8, x8, #1str x8, [x0]

    甚至如果写操作都需要等待,还可能将写操作都滞后:

    ldr x9, [x1]mov x9, #1ldr x8, [x0]add x8, x8, #1str x9, [x1]str x8, [x0]

    再或者如果加法器繁忙,又会带来全新的执行顺序,当然这一切都要建立在被重新排序的指令之间不能相互他们依赖执行的结果。

    副作用

    指令重排大幅度提升了 CPU 的执行速度,但凡事都有两面性,虽然在 CPU 层面重排的指令能保证运算的正确性,但在逻辑层面却可能带来错误。比如常见的自旋锁场景,我们可能设置一个 bool 类型的 flag 来自旋等待某异步任务的完成,在这种情况下,一般是在任务结束时对 flag 置位,如果置位 flag 的语句被重排到异步任务语句的中间,将会带来逻辑错误。下面我们会通过一个实验来直观展示指令重排带来的副作用。

    一个实验

    在下面的代码中我们设置了两个线程,一个执行运算,并在运算结束后置位 flag,另一个线程自旋等待 flag 置位后读取结果。

    我们首先定义一个保存运算结果的结构体。

    typedef struct FlagsCalculate {    int a;    int b;    int c;    int d;    int e;    int f;    int g;} FlagsCalculate;

    为了更快的复现重排带来的错误,我们使用了多个 flag 位,存储在结构体的 e, f, g 三个成员变量中,同时 a, b, c, d 作为运算结果的存储变量:

    int getCalculated(FlagsCalculate *ctx) {    while (ctx->e == 0 || ctx->f == 0 || ctx->g == 0);    return ctx->a + ctx->b + ctx->c + ctx->d;}

    为了更快的触发未命中缓存,我们使用了多个全局变量;为了模拟加法器和乘法器繁忙,我们采用了密集的运算:

    int mulA = 15;int mulB = 35;int divC = 2;int addD = 20;void calculate(FlagsCalculate *ctx) {    ctx->a = (20 * mulA - mulB) / divC;    ctx->b = 30 + addD;    for (NSInteger i = 0; i < 10000; i++) {        ctx->a += i * mulA - mulB;        ctx->a *= divC;        ctx->b += i * mulB / mulA - mulB;        ctx->b /= divC;    }    ctx->c = mulA + mulB * divC + 120;    ctx->d = addD + mulA + mulB + 5;    ctx->e = 1;    ctx->f = 1;    ctx->g = 1;}

    接下来我们将他们封装在 pthread 线程的执行函数内:

    void* getValueThread(void *arg) {    pthread_setname_np("getValueThread");    FlagsCalculate *ctx = (FlagsCalculate *)arg;    int val = getCalculated(ctx);    assert(val == -276387);    return NULL;}void* calValueThread(void *arg) {    pthread_setname_np("calValueThread");    FlagsCalculate *ctx = (FlagsCalculate *)arg;    calculate(ctx);    return NULL;}void newTest() {    FlagsCalculate *ctx = (FlagsCalculate *)calloc(1, sizeof(struct FlagsCalculate));    pthread_t get_t, cal_t;    pthread_create(&get_t, NULL, &getValueThread, (void *)ctx);    pthread_create(&cal_t, NULL, &calValueThread, (void *)ctx);    pthread_detach(get_t);    pthread_detach(cal_t);}

    每次调用 newTest 即开始一轮新的实验,在 flag 置位未被乱序执行的情况下,最终的运算结果是 -276387,通过短时间内不断并发执行实验,观察是否遇到断言即可判断是否由重排引发了逻辑异常:

    while (YES) {    newTest();}

    笔者在一个 iOS Empty Project 中添加上述代码,并将其运行在一台 iPhone XS Max 上,约 10 分钟后,遇到了断言错误:8ba0a3bcf3aea825419deb27df779b3a.png

    显然这是由于乱序执行导致的 flag 全部被提前置位,从而导致异步线程获取到的执行结果错误,通过实验我们验证了上面的理论。

    答疑解惑

    看到这里你可能惊出一身冷汗,开始回忆起自己职业生涯中写过的类似逻辑,也许线上有很多正在运行,但从来没出过问题,这又是为什么呢?

    在 iOS 开发中,我们常使用 GCD 作为多线程开发的框架,这类 High Level 的多线程模型本身已经提供好了天然的内存屏障来保证指令的执行顺序,因此可以大胆的去写上述逻辑而不用在意指令重排,这也是我们使用 pthread 来进行上述实验的原因。

    到这里你也应该意识到,如果采用 Low Level 的多线程模型来进行开发时,一定要注意指令重排带来的副作用,下面我们将介绍如何通过内存屏障来避免指令重排对逻辑的影响。

    内存屏障

    简介

    内存屏障是一条指令,它能够明确地保证屏障之前的所有内存操作均已完成(可见)后,才执行屏障后的操作,但是它不会影响其他指令(非内存操作指令)的执行顺序[3]。

    因此我们只要在 flag 置位前放置内存屏障,即可保证运算结果全部写入内存后才置位 flag,进而也就保证了逻辑的正确性。

    放置内存屏障

    我们可以通过内联汇编的形式插入一个内存屏障:

    void calculate(FlagsCalculate *ctx) {    ctx->a = (20 * mulA - mulB) / divC;    ctx->b = 30 + addD;    for (NSInteger i = 0; i < 10000; i++) {        ctx->a += i * mulA - mulB;        ctx->a *= divC;        ctx->b += i * mulB / mulA - mulB;        ctx->b /= divC;    }    ctx->c = mulA + mulB * divC + 120;    ctx->d = addD + mulA + mulB + 5;    __asm__ __volatile__("dmb sy");    ctx->e = 1;    ctx->f = 1;    ctx->g = 1;}

    随后继续刚才的试验可以发现,断言不会再触发异常,内存屏障限制了 CPU 乱序执行对正常逻辑的影响。

    volatile 与内存屏障

    我们常常听说 volatile 是一个内存屏障,那么它的屏障作用是否与上述 DMB 指令一致呢,我们可以试着用 volatile 修饰 3 个 flag,再做一次实验:

    typedef struct FlagsCalculate {    int a;    int b;    int c;    int d;    volatile int e;    volatile int f;    volatile int g;} FlagsCalculate;

    结果最后触发了断言异常,这是为何呢?因为 volatile 在 C 环境下仅仅是编译层面的内存屏障,仅能保证编译器不优化和重排被 volatile 修饰的内容,但是在 Java 环境下 volatile 具有 CPU 层面的内存屏障作用[4]。不同环境表现不同,这也是 volatile 让我们如此费解的原因。

    在 C 环境下,volatile 常常用来保证内联汇编不被编译优化和改变位置,例如我们通过内联汇编放置一个编译层面的内存屏障时,通过 __volatile__ 修饰汇编代码块来保证内存屏障的位置不被编译器改变:

    __asm__ __volatile__("" ::: "memory");

    总结

    到这里,相信你对指令重排和内存屏障有了更加清晰的认识,同时对 volatile 的作用也更加明确了,希望本文能对大家有所帮助。

    参考资料

    [1]

    缓存一致性(Cache Coherency)入门: https://www.infoq.cn/article/cache-coherency-primer

    [2]

    CPU Reordering – What is actually being reordered?: https://mortoray.com/2010/11/18/cpu-reordering-what-is-actually-being-reordered/

    [3]

    ARM Information Center - DMB, DSB, and ISB: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489c/CIHGHHIE.html

    [4]

    volatile 与内存屏障总结: https://zhuanlan.zhihu.com/p/43526907

    展开全文
  • 关于指令重排内存屏障和总线风暴

    千次阅读 2019-09-18 22:41:59
    指令重排 java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果它顺序化情况的结果相等,那么指令的执行顺序可以代码顺序不一致,此过程叫指令的重排序。 从源码到最后执行的指令序列过程是: 源码...

    指令重排

    java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
    从源码到最后执行的指令序列过程是:
    源码->编译器优化重排序->指令级并行重排序->内存系统重排序->最后执行的指令序列

    内存屏障

    内存屏障可以禁止指令重排序
    在这里插入图片描述
    从上图可以看出:
    1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
    2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
    3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
    JMM内存屏障插入策略:
    1)在每个volatile写操作的前面插入一个StoreStore屏障。
    2)在每个volatile写操作的后面插入一个StoreLoad屏障。
    3)在每个volatile读操作的后面插入一个LoadLoad屏障。
    4)在每个volatile读操作的后面插入一个LoadStore屏障。

    总线风暴

    由于volatile的mesi缓存一致性协议需要不断的从主内存嗅探和cas不断循环无效交互导致总线带宽达到峰值
    解决办法:部分volatile和cas使用synchronize

    展开全文
  • 一、指令重排 什么是指令重排?      java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果 它顺序化情况的结果相等,那么指令的执行顺序可以代码顺序不一致,此过程叫指令的重排序。 指令重排...

    一、指令重排

    什么是指令重排?
         java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与 它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

    指令重排发生阶段?

    • 执行器编译阶段
    • CPU运行时

    指令重排的意义
         适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

    源码到最终执行的指令序列示意图
    在这里插入图片描述
    指令重排遵循的原则
         指令重排遵循 as-if-serial 语义1

    指令重排的测试

    public class VolatileReOrderSample {
        private static int x = 0, y = 0;
        private static int a = 0, b = 0;
        // 添加volatile关键字禁止指令重排
    	// private static volatile int a = 0, b = 0;
        static Object object = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            int i = 0;
            for (;;) {
                i++;
                x = 0; y = 0;
                a = 0; b = 0;
    
                Thread thread1 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 这个值需要根据自己的机器去试,尽可能的使两个线程的代码执行时间一致
                        shortWait(23500);
                        a = 1;
                        // 如果不添加volatile关键字,可以通过UnSafe添加内存屏障
                        x = b;
                    }
                });
    
                Thread thread2 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        b = 1;
                        y = a;
                    }
                });
    
                thread1.start();
                thread2.start();
                thread1.join();
                thread2.join();
    
                /*
                 * 由于a = 1;b = 1;操作分别在 x = b; y = a;操作的前边,正常情况下是不可能出现(0,0)这种情况
                 *
                 * 出现这种情况说明y = a;或者x = b;在b = 1;或者a = 1;之前执行的
                 *
                 * 也就是说CPU或者JIT进行了指令重排
                 */
                String result = "第" + i + "次 (" + x + ", " + y +")";
                if (x == 0 && y == 0) {
                    System.err.println(result);
                    break;
                } else {
                    System.out.println(result);
                }
            }
        }
        
        private static void shortWait(int interval) {
            long start = System.nanoTime();
            long end = start;
            while (end - start < interval) {
                end = System.nanoTime();
            }
        }
    }
    

    添加volatile关键字,由于volatile禁止指令重排,上述程序中的for循环不再break;
    volatile通过内存屏障实现禁止指令重排。

    内存屏障

    内存屏障:又称内存栅栏,是一个CPU指令

    内存屏障的作用

    • 保证特定操作的执行顺序,
    • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

         由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

    JMM针对编译器制定的volatile重排序规则表
    在这里插入图片描述

    • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后;
    • ∙当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前;
    • ∙当第一个操作是volatile写,第二个操作是volatile读时,不能重排序;

         为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数 几乎不可能。为此,JMM采取保守策略。
    下面是基于保守策略的JMM内存屏障插入策略。 ∙

    • 在每个volatile写操作的前面插入一个StoreStore屏障;
    • 在每个volatile写操作的后面插入一个StoreLoad屏障;
    • 在每个volatile读操作的后面插入一个LoadLoad屏障;
    • 在每个volatile读操作的后面插入一个LoadStore屏障;

    1. as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义 ↩︎

    展开全文
  • 关于指令重排内存屏障和总线风暴 指令重排 java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果它顺序化情况的结果相等,那么指令的执行顺序可以代码顺序不一致,此过程叫指令的重排序。 从源码到...
  • cpu指令重排是怎么回事? cpu指令重排? package com.dym.juc; import java.util.HashSet; import java.util.Set; public class VolatileSerialTest { static int x=0; static int y=0; static int a=0;...
  • 文章目录CPU运行时优化(高速缓存、指令重排等)一、CPU高速缓存缓存一致性协议1.1 高速缓存1.2 缓存一致性协议(MESI)二、CPU运行时指令重排三、内存屏障 一、CPU高速缓存缓存一致性协议 1.1 高速缓存   CPU...
  • 线程、工作内存与主存之间的关系 内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外) lock (锁定...
  • Xcode 工程中嵌入汇编代码iOS 汇编入门教程(三)汇编中的 Section 数据存取iOS 汇编教程(四)基于 LLDB 动态调试快速分析系统函数的实现iOS 汇编教程(五)Objc Block 的内存布局和汇编表示前言具有 ARM 体系结构的...
  • volatile与内存屏障

    2021-01-27 15:21:52
    2.局部限制指令重排指令重排存在多种情况:1.编译器重排;2.处理器并行重排;3.因为store buffer,invalid queue等异步机制的存在导致的内存重排) 为了达到这两个目的,volatile做了两件事情: 1.禁止编译器的...
  • CPU指令重排与缓存一致性

    千次阅读 2018-10-22 15:08:32
    指令重排无法逾越内存屏障。 缓存一致性的实现,先看看x86的cpu缓存结构。 1、各种寄存器,用来存储本地变量和函数参数,访问一次需要1cycle,耗时小于1ns; 2、L1 Cache,一级缓存,本地core的缓存,分成32K的数据...
  • CPU缓存与内存屏障

    2021-05-05 19:21:35
    一.CPU缓存 三级缓存 缓存同步协议 二.运行时指令重排 三.两个问题 四.内存屏障
  • 前言指令重排内存屏障多处理器指南 指令重排 对于编译器的编写者来说,Java内存模型(JMM)主要是由禁止指令重排的规则所组成的,其中包括了字段(包括数组中的元素)的存取指令和监视器(锁)的控制指令。 ...
  • volatile与内存屏障总结

    千次阅读 2019-09-08 19:18:32
    内存屏障 Memory Barrior 1.1 重排序 同步的目的是保证不同执行流对共享数据并发操作的一致性。在单核时代,使用原子变量就很容易达成这一目的。甚至因为CPU的一些访存特性,对某些内存对齐数据的读或写也具有...
  • Java内存模型及...什么是内存屏障?它解决了什么问题? volatile 如何防止指令重排? volatile 如何保持内存可见性? volatile 能保证原子性么? 既然 CPU 有 MESI ,为什么 Java内存模型还需要 volatile 关键字
  • (2)防止指令重排。而针对变量的可见性我们知道是读volatile变量的时候直接从内存中读,而写volatile变量的时候直接写入内存。那么重排序呢?2重排序2.1定义所谓重排序是指编译器和处理器为了提高程序的执行效率,在...
  • java内存模型 内存屏障

    千次阅读 2014-12-16 09:04:44
    但在多核处理器上通常需要使用内存屏障指令来确保这种一致性。即使编译器优化掉了一个字段访问(例如,因为一个读入的值未被使用),这种情况下还是需要产生内存屏障,就好像这个访问仍然需要保护。(可以参考下面的...
  • 内存屏障 Memory Barrior1.1 重排序同步的目的是保证不同执行流对共享数据并发操作的一致性。在单核时代,使用原子变量就很容易达成这一目的。甚至因为CPU的一些访存特性,对某些内存对齐数据的读或写也具有原子的...
  • 内存屏障指令仅直接控制CPU与其缓存之间,垃圾回收机制中“写屏障(write barriers)”无关。 一、重排序 编译器或者CPU的代码的结构重排排序,达到最佳效果。 (1)编译器重排 CPU只读一次的x和y值。不需...
  • 推荐阅读: 这套Github上40K+star学习笔记,可以帮你搞定95%以上的Java面试 ...volatile关键字在 Java 中的作用是保证变量的可见性和防止指令重排。 一、保证变量的可见性 在知道volatile是如何
  • Java 内存屏障

    千次阅读 2016-12-27 23:29:28
    为什么需要内存屏障我们知道,在多CPU(核)场景下,为了充分利用CPU,会通过流水线将指令并行进行。为了能并行执行,又需要将指令进行重排序以便进行并行执行,那么问题来了,那些指令不是在所有场景下都能进行重排...
  • 内存屏障主要用于防止指令重排而导致的无法预测的情况。代码经过编译器生成的指令并不一定都是按着我们原先的想法来生成的,可能经过优化等情况进行了指令的重排,然而这些重排在执行后的结果应当是一致的。其实及时...
  • 指令重排 内存屏障 多处理器 指南 编译器和处理器必须同时遵守重排规则。由于单核处理器能确保“顺序执行”相同的一致性,所以在单核处理器上并不需要专门做什么处理,就可以保证正确的执...
  • 深入浅出Linux内核中的内存屏障

    千次阅读 2021-01-16 17:17:35
    指令重排 每个 CPU 运行一个程序,程序的执行产生内存访问操作。在这个抽象 CPU 中,内存 操作的顺序是松散的,CPU 假定进程间不依靠内存直接通信,在不改变程序执行 结果 的推测下由自己方便的顺序执行内存访问操作...
  • 编译器在编译时进行了编译优化,导致指令重排; 在多cpu环境下,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。在这种模型下会存在一个现象,即缓存中的数据内存的数据...
  • 内存屏障与指令重排的相关论证 Hotspot VM 的相关介绍 通过对于这些基础模块的学习或许能让你对于并发编程的底层知识有新的理解和认知。 当前内容版权归码字科技所有并授权显示,盗版必究。阅读原文
  • ゼロから始める并发世界生活——第二卷:Java内存模型并发三大特性JMM模型主内存工作内存Java内存模型硬件内存架构的关系JMM存在的必要性数据同步八大原子操作同步规则分析并发的三大特性指令重排现像三大特性之...

空空如也

空空如也

1 2 3
收藏数 58
精华内容 23
关键字:

内存屏障与指令重排