02、重排序与顺序一致性

前言

在我们编写程序并运行的时候,编译器给我们一个错觉:程序编译的顺序与编写的顺序是一致的。但是实际上,为了提高性能,编译器和处理器常常会对指令进行重排序。重排序主要分为两类:编译器优化的重排序、指令级别并行的重排序和内存系统的重排序。所以我们编写好Java源代码之后,会经过以上三个重排序,到最终的指令序列。我们这里提到的Java内存模型又是什么呢?Java内存模型(后面简称JMM)是语言级别的内存模型,主要用于控制一个共享变量的写入何时对另一个线程可见(后面所有方面都是围绕这点展开的)。JMM可以确保在不同的处理器平台和编译器之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性的保证。

JMM对重排序的处理

对于编译器的重排序,JMM会根据重排序规则禁止特定类型的编译器重排序;对于处理器重排序,JMM会插入特定类型的内存屏障,通过内存的屏障指令禁止特定类型的处理器重排序。这里讨论JMM对处理器的重排序,为了更深理解JMM对处理器重排序的处理,先来认识一下常见处理器的重排序规则:

处理器\规则 Load-Load Load-Store Store-Store Store-Load 数据转换
SPARC-TSO N N N Y N
x86 N N N Y N
IA64 Y Y Y Y N
SPARC-TSO Y Y Y Y N

其中的N标识处理器不允许两个操作进行重排序,Y表示允许。其中Load-Load表示读-读操作、Load-Store表示读-写操作、Store-Store表示写-写操作、Store-Load表示写-读操作。可以看出:常见处理器对写-读操作都是允许重排序的,并且常见的处理器都不允许对存在数据依赖的操作进行重排序(对应上面数据转换那一列,都是N,所以处理器不允许这种重排序)。

那么这个结论对我们有什么作用呢?比如第一点:处理器允许写-读操作两者之间的重排序,那么在并发编程中读线程读到可能是一个未被初始化或者是一个NULL等,出现不可预知的错误,基于这点,JMM会在适当的位置插入内存屏障指令来禁止特定类型的处理器的重排序。内存屏障指令一共有4类:

1、 LoadLoadBarriers:确保Load1数据的装载先于Load2以及所有后续装载指令;
2、 StoreStoreBarriers:确保Store1的数据对其他处理器可见(会使缓存行无效,并刷新到内存中)先于Store2及所有后续存储指令的装载;
3、 LoadStoreBarriers:确保Load1数据装载先于Store2及所有后续存储指令刷新到内存;
4、 StoreLoadBarriers:确保Store1数据对其他处理器可见(刷新到内存,并且其他处理器的缓存行无效)先于Load2及所有后续装载指令的装载该指令会使得该屏障之前的所有内存访问指令完成之后,才能执行该屏障之后的内存访问指令;

重排序

重排序指的是编译器和处理器为了优化程序性能而对指令序列进行重新排序的手段。根据上面的表格,得到了处理器不会对存在数据依赖的操作进行重排序。这里数据依赖的准确定义是:如果两个操作同时访问一个变量,其中一个操作是写操作,此时这两个操作就构成了数据依赖。常见的具有这个特性的如i++、i–。如果改变了具有数据依赖的两个操作的执行顺序,那么最后的执行结果就会被改变。这也是不能进行重排序的原因。

as-if-serial语义

是不是有点突兀,怎么突然蹦出这个玩意。还是先解释这个语义的含义吧:不管怎么重排序,单线程程序的执行结果不能发生改变。编译器、Runtime和处理器也是如此。这个语义相当于把单线程保护起来了,所以即使编译器和处理器对指令序列进行了重排序,我们也会认为程序指令并没有发生重排序,也就出现了篇首的幻觉。

重排序对多线程的影响

如果代码中存在控制依赖的时候,会影响指令序列执行的并行度(因为高效)。也是为此,编译器和处理器会采用猜测(Speculation)执行来克服控制的相关性。所以重排序破坏了程序顺序规则(该规则是说指令执行顺序与实际代码的执行顺序是一致的,但是处理器和编译器会进行重排序,只要最后的结果不会改变,该重排序就是合理的)。

重排序的小结

在单线程程序中,对存在依赖关系的操作进行重排序,不会改变最后的执行结果;在多线程程序中,对存在依赖关系的操作进行重排序,可能会改变最后的执行结果。

顺序一致性

顺序一致性实际上指的是一个内存模型,JMM对顺序一致模型进行了更严格的规定。所以JMM是以顺序一致性模型进行参照的。

数据竞争
如果程序没有正确同步,那么可能会存在数据竞争。JMM对数据竞争的定义如下:

1、 在一个线程中写一个变量;
2、 在另一个线程中读取同一个变量;
3、 而且写和读没有通过同步来排序;

那么,反过来说,如果一个多线程程序已经正确同步,这个程序将是一个没有数据竞争的程序。JMM是如何保证这点的呢?

如果程序是正确同步的,程序的执行将有顺序一致性——也就是说程序的执行结果与该程序在顺序一致内存模型中的执行结果是一致的。

顺序一致性模型

顺序一致性模型有以下两大特性:

1、 一个线程中的所有操作必须按照程序的顺序来执行;
2、 (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序在顺序一致内存模型中,每一个操作都必须是院子执行且立即对所有线程可见;

可以把顺序顺序一致模型理解为一个单摆,每一个时刻单摆只能到一个位置,对应过来,任何时刻最多只能有一个线程才能连接到内存。由于重排序的影响,实际指令的执行顺序是不可知的,但是不管如何排序,每个操作能够立即对其他线程可见,所以所有线程看到的都是一样的执行顺序。但是在JMM中是没有这个规定的,就是说其他线程看到执行顺序与除自己外的线程看到的执行顺序可能是不一致的。比如,当前线程把写过的数据缓存缓存到写缓存中,在没有刷新到主内存(计算机系统的DRAM)之前,这个写操作对其他线程是不可见的,意味着其他线程认为该线程根本没有执行写操作。那么何时才能可见呢?只有在当前线程把写缓存中数据刷新到主内存的时候,对其他内存才是可见的。