Loading

多线程——一些概念...

多线程——一些概念...

指令重排序

指令是CPU可以完成的动作,比如传送,运算,控制,比较等等。一行简单的代码往往需要多条指令(执行多个动作)来实现。如下图所示:

C汇编代码

​ 左边的C代码多了一行

int b = a + 1;

​ 但是汇编代码多出了三行,可以看出有两种类型的动作:移动(传送)和相加

mov     eax, DWORD PTR [rbp-4]
add     eax, 1
mov     DWORD PTR [rbp-8], eax

在线反汇编代码网站

重排序是重新排列指令的先后顺序。重排序在源码到程序运行这一过程中都存在,很明显目的是为了程序优化运行。这一过程中发生的重排序,如下图所示:

img

  1. 编译器优化重排序

​ 编译器重排序是在生成汇编代码时,对按源码顺序生成的汇编指令进行重排序,前提是这个过程不会改变单线程的程序语义。可参考下图示例:

image-20201118220045886

​ 图中对C代码,采用arm64 gcc7.3 -O2进行优化编译,结果是汇编代码进行了重排序。

  1. 指令级并行重排序

​ 现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  1. 内存系统重排序

​ 由于处理器使用缓存和读/写缓冲区,这使得加载(读)和存储(写)操作看上去可能是在乱序执行。下面将说明这一情况:

image-20201118222255331

image-20201118222410898

​ 从上图可以看到处理器的写操作是:数据写入缓冲区->缓冲器写入内存。如果没有写缓冲区,那么CPU准备直接往内存写入数据时会停顿(内存的运行速度远低于CPU),极大影响工作效率。另外,可以通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。但是每个处理器的缓冲区只对它所在的处理器可见,所以这里出现了缓冲一致性问题

​ 所以我们可以上图的指令执行流程可能是:

步骤 a b x y
A1 B1 0 0 - -
A2 B2 0 0 0 0
A3 B3 1 2 0 0

小结:指令重排序一方面优化了指令执行的效率,另一方面在多线程场景下也带来了困扰。因此禁止指令重排序对于保证程序正确运行得到预期结果就至关重要了。

内存屏障

​ 内存屏障正是用来禁止指令重排序的工具。内存屏障是特定于硬件的,又可分为显式和隐式。在volatile介绍文章中谈到使用volatile关键字后,写操作变成了lock cmpxchg,其中lock前缀就是一个隐式的内存屏障。此外,也存在显式的内存屏障,例如在x86 Xeon对使用了volatile变量写操作前会执行mfence操作,这是一个显式双向内存屏障。

  内存屏障是存在于读(Load)或写(Store)操作之间,在四种场景下:读读操作,读写操作,写写操作,写读操作。Load:数据装载,Store:刷新内存。

类型 示例 说明(屏障作用)
LoadLoad Barriers Load1;~;Load2 保证第一个Load1会在第二个Load2及后续Load之前执行
LoadStore Barriers Load1;~;Store1 保证第一个Load1会在第二个Store1及后续Store之前执行
StoreStore Barriers Store1;~;Store2 保证第一个Store1会在第二个Store2及后续Store之前执行
StoreLoad Barriers Store1;~;Load1 保证第一个Store1会在第二个Load1及后续Load之前执行

  StoreLoad Barriers是一个“全能型”屏障,它可以保证屏障之前的Load和Store指令全部执行完成,才去执行屏障之后的指令。执行该屏障要把之前所有Load和Store指令完成,往往会把写缓冲区的数据全部刷新到内存中。

happens-before

  happens-before说的是两个操作之间的一种关系。例如,操作1happens-before操作2,那么操作1的结果对操作2是可见的。JMM(JSR-133)采用了这样一种概念来体现内存可见性,详细描述是:如果一个操作的结果需要对另一个操作可见,那么两个操作之间必须存在happens-before关系。注意,这里和两个操作之间的执行顺序无关,只关注结果的可见性。那么happens-before关系的具体表现是什么呢?也就是什么情况下的两个操作会具有happens-before关系?

下面是happens-before关系出现的场景(JMM约定在这种场景下的两个操作具有happens-before关系):

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个锁unlock操作先行发生于后面的对同一个锁的lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果 A happens- before B,且 B happens- before C ,那么A happens- before C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

  有了上述happens-before的规则或则说场景,那么我们就能利用此来按我们能够编写出能正确运行得到预期结果的代码了。实际上happens-before是JMM提供给程序员的视图,便于多线程场景的编程,它实际还是为了解决指令重排序的问题。

image-20201119223452788

  锁是实现同步机制的工具。同步机制:某一操作在同一时刻只能一个线程执行。给某一操作上锁就代表这一操作正在被某一线程执行,其他线程发现了操作上的锁就无法执行该操作。

参考

  1. 深入理解Java内存模型
posted @ 2020-11-18 23:18  齐玉  阅读(70)  评论(0)    收藏  举报