Java并发:CPU

CPU

概述

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。而重排序对于Java并发存在一定的影响

指令重排序

从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送发送给各相应电路单元处理。但并不是说指令任意重排,CPU需要能够正确处理指令依赖情况以保障程序能够得出正确的执行结果。

指令重排序的一个原则是不违反数据依赖性

数据依赖性

数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作间就存在数据依赖性。(只考虑单个处理器中执行的指令序列和单个线程中执行的操作)

分类:

  • 写后读。a=1;b=a;
  • 写后写。a=1;a=2;
  • 读后写。a=b;b=1;

在数据依赖性下,只要重排序两个操作的执行顺序,程序的执行结果就会改变。

as-if-serial语义

as-if-serial语义指:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但如果操作间不存在数据依赖关系,则这些操作可能被重排序。

as-if-serial语义将单线程保护了起来,编译器、runtime、处理器共同为单线程程序员创造了一个幻觉,即单线程程序是按照程序的顺序来执行的。因此单线程程序员无需担心重排序、内存可见性问题。

程序顺序规则

对于代码段

1
2
3
double pi=3.14;
double r=1.0;
double area=pi*r*r;

根据程序顺序规则,则有

A happens-before B、B happens-before C、A happens-before C

  • 但是在实际操作中,B可以排在A之前执行
    • 这里的A结果不需要对B可见,因此这种重排序并不非法,JMM允许这种重排序。
  • A happens-before B,JMM仅仅要求前一个操作执行的结果对后又改操作可见,且前一个操作按顺序排在第二个操作之前。

软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。

重排序对多线程的影响

但在多线程程序中,对存在控制依赖的操作会产生重排序,因为它们存在与不同的线程当中,可能会改变程序的执行结果。

从源代码到指令序列的重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。

  • 编译器重排序:编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 处理器重排序:
    • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

对于Java源代码到最终执行的指令序列,会经历三种重排序,可能导致内存可见性问题:

1555057714237

JMM属于语言级别的内存模型,确保在不同的编译器和处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,保证一致性的内存可见性。

  • 对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序
  • 对于处理器重排序。JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,禁止特定类型的处理器重排序

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map configOptions;
char[] configText;
boolean initialized = false;
//假设以下代码在线程A中执行
//则由于指令重排序,首先initizlized = true,之后才进行读取configText
configOptions = new HashMap();
configText = readConfigFile(fileName);
initizlized = true;

//假设以下代码在线程B中执行
//由于受重排序影响,initizlized=true的时候,并没有初始化配置信息,因此之后的动作错误。
while(!initialized){
sleep();
}
doSomethingWithConfig();

参考