在高并发模型中,无论面对物理机SMP系统模型还是面对虚拟机多线程并发内存模型(例如JVM),指令重排(编译器,运行时)和内存障碍都非常重要,因此了解这些概念和原则很重要。否则,您很难确定哪些操作是绝对安全的并发操作?哪些相对安全?哪种并发同步方法性能最低? valotile的两种语义是什么?还有很多。我最初计划自己撰写一篇有关JVM内存模型的博客文章。后来,当我整理信息时,偶然发现了一篇很好的相关文章(来自美团审查小组)。我个人觉得这篇文章写得更全面,至少是在概念上。我说得很清楚,并转载给所有人。原始地址:一、什么是重新排序
请先看这段代码
1 public class PossibleReordering { 2 static int x = 0, y = 0; 3 static int a = 0, b = 0; 4 5 public static void main(String[] args) throws InterruptedException { 6 Thread one = new Thread(new Runnable() { 7 public void run() { 8 a = 1; 9 x = b; 10 } 11 }); 12 13 Thread other = new Thread(new Runnable() { 14 public void run() { 15 b = 1; 16 y = a; 17 } 18 }); 19 one.start();other.start(); 20 one.join();other.join(); 21 System.out.println(“(” + x + “,” + y + “)”); 22 }
很容易认为此代码的运行结果可能是(1,0),(0,1)或(1,1)),因为可以在第二个线程启动之前执行第一个线程,所以它相反,这两条指令也可能同时或交替执行。
但是,该代码的执行结果也可能是(0,0)。因为在实际操作中,可能不会严格按照代码语句的顺序执行代码指令。(0,0)语句执行过程如下图所示,值得注意的是,两个语句a = 1和x = b的赋值操作顺序已经颠倒,换句话说,发生了“重新排序”指令。 (事实,在上面,此结果的输出不一定表示发生了指令重新排序。内存可见性问题也将导致此类输出。有关详细信息,请参见以下内容)。
对重新排序现象不太了解的开发人员可能会对这种现象感到惊讶,但是在作者的开发环境中进行的一项小型实验证实了这一结果。
实验代码是构造一个循环并重复执行上述示例代码,直到出现a = 0和b = 0的输出。实验结果表明,第13830次执行循环时,输出(0,0)。
大多数现代微处理器将采用无序执行(OoOE或OOE)方法,如果条件允许,它们将直接运行当前能够立即执行的后续指令。避免因获取下一条指令所需的数据而导致等待。通过无序执行技术,处理器可以大大提高执行效率。
除了处理器之外,Java运行时环境的通用JIT编译器还将执行指令重新排序,即生成的机器指令与字节码指令序列不一致。
二、as-if-serial语义
按序列表示的语义意味着可以对所有操作(Action)进行重新排序以进行优化,但是必须确保它们的重新排序结果与程序代码本身的预期结果一致。 Java编译器,运行时和处理器都保证在单线程下的as-if-serial语义。
例如,为了确保这种语义,在与数据相关的操作中将不会发生重新排序。
int a = 1; int b = 2; int c = a + b;
将以上代码编译为Java字节码或生成机器指令可以视为以下步骤(某些步骤在实践中可能会省略或添加)。
将值1分配给b将值2分配为a的值并将b的值取为两个值并将它们加到c
在上述5个动作中,动作1可以用动作2、4重新排序,动作2可以用动作1、3重新排序,动作3可以用动作2、重新排序。 4。 4可以用1、3重新排序。但是动作1和动作3、5无法重新排序。动作2和动作4、5不能重新排序。由于它们之间的数据依赖性,一旦重新排列,就不能保证as-if-serial语义。
为了确保as-if-serial语义,Java异常处理机制还将对重新排序进行一些特殊处理。例如,在下面的代码中,y = 0/0可能会在x = 2之前重新排序并执行。为了确保最终不会输出x = 1的错误结果,JIT将在其中插入错误代码在对补偿代码重新排序时,请在catch语句中,将x分配给2以将程序恢复到发生异常时应具有的状态。这种方法确实使异常捕获的逻辑复杂化,但是JIT的优化原理是即使捕获块逻辑变得复杂,也要尝试在正常操作下优化代码逻辑。毕竟,进入catch块是一回事。出现“异常”情况。
1 public class Reordering { 2 public static void main(String[] args) { 3 int x, y; 4 x = 1; 5 try { 6 x = 2; 7 y = 0 / 0; 8 } catch (Exception e) { 9 } finally { 10 System.out.println("x = " + x); 11 } 12 } 13 }

三、内存访问重新排序和内存可见性
在计算机系统中,为了尽可能避免处理器访问主内存的时间开销,处理器通常使用缓存来提高性能。该模型如下图所示。
在此模型下,存在以下现象:高速缓存中的数据和主内存中的数据未实时同步,并且CPU(或CPU内核)之间高速缓存的数据未实时同步。结果,在同一时间点,每个CPU看到的相同内存地址处的数据值可能不一致。从程序的角度来看,在同一时间,每个线程看到的共享变量的值可能不一致。
一些意见认为此现象是一种重新排序,并将其命名为“内存系统重新排序”。这种内存可见性问题的结果就像是对内存访问指令进行了重新排序。
这种内存可见性问题也将导致第1章中的示例代码执行(0,0),即使没有指令重新排序。
四、内存访问重新排序和Java内存模型
Java的目标是成为独立于平台的语言,即编写一次即可在任何地方运行。但是,在不同的硬件环境中重新排序指令的规则是不同的。例如,在x86下正常运行的Java程序在IA64下可能会得到意外的结果。为此,JSR-1337制定了Java内存模型(Java Memory Model,JMM),旨在提供统一的参考规范和屏蔽平台差异。从Java 5开始,Java内存模型已成为Java语言规范的一部分。
根据Java内存模型中的规定,可以总结以下规则。在发生之前和之后的两个操作将不会重新排序,并且前者的记忆对后者可见。
之前发生关系只是Java内存模型的近似描述。它不够严格,但是便于日常程序开发和参考。有关更严格的Java内存模型的定义和描述,请阅读JSR-133原始文本或Java语言规范的第1章7.4。
此外,Java内存模型扩展了volatile和final的语义。易失性语义的扩展确保了在某些情况下不会对易失性变量进行重新排序。易失性64位变量double和long的读取和分配操作都是原子的。最终语义的扩展确保必须在对象的构造方法结束之前初始化所有最终成员变量(前提是此引用不会溢出)。
下表总结了Java内存模型的重新排序规则。
表中“第二个操作”的含义是指第一个操作之后的所有指定操作。例如,普通读不能与所有后续的易失写重新排序。此外,JMM还规定,上述易失性和同步块规则适用于存在多线程访问的场景。例如,如果编译器(此处的编译器还包括JIT,下同)证明易失性变量只能由单个线程访问,则可以将其视为普通变量。
空白单元格表示允许重新排序,而不会违反Java的基本语义。例如,编译器不会将读取和写入操作重新排序到相同的内存地址,而是允许对不同地址进行读取和写入操作。
此外,为了确保final的新语义。 JSR-133还限制了最终变量的重新排序。
五、记忆障碍
内存屏障(Memory Barrier,有时称为Memory Fence,Memory Fence)是一种CPU指令,用于控制特定条件下的重新排序和内存可见性问题。 Java编译器还禁止根据内存屏障的规则进行重新排序。
记忆障碍可以分为以下几种类型

LoadLoad屏障:对于此类语句Load1; LoadLoad; Load2,在访问要由Load2读取的数据和后续的读取操作之前,请确保已读取要由Load1读取的数据。
StoreStore障碍:对于此类语句Store1; StoreStore;在执行Store2和后续写入操作之前,必须确保Store2的写入操作对其他处理器可见。
LoadStore障碍:对于此类语句,Load1; LoadStore;在清空Store2和后续的写操作之前,请确保Store2已被Load1读取的数据已被读取。
StoreLoad障碍:对于此类语句Store1; StoreLoad;在执行Load2和所有后续读取操作之前,请确保对Load2进行存储2写入对于所有处理器都是可见的。它的开销是四个障碍中最大的。在大多数处理器实现中,此屏障是通用屏障,可充当其他三个内存屏障的功能。
某些处理器具有严格的重新排序规则,并且可以在没有内存障碍的情况下很好地工作。在这种情况下,Java编译器不会设置内存屏障。
为了实现上一章中讨论的JSR-133要求,Java编译器以这种方式使用内存屏障。
为了确保final字段的特殊语义,还将在以下语句中添加一个内存屏障。
x.finalField = v; StoreStore; sharedRef = x;
六、英特尔64 / IA-32架构下的内存访问重新排序
Intel 64和IA-32是我们更常用的硬件环境。与其他处理器相比,它们具有更严格的重新排序规则。 Pentium 4之后的Intel 64或IA-32的重新排序规则如下。 9
在单个CPU系统中
在多处理器系统中
值得注意的是,对于Java编译器,采用Intel 64 / IA-32架构的处理器不需要LoadLoad,LoadStore和StoreStore障碍,因为这三个障碍的重新排序不会发生。
七、英特尔64 / IA-32架构下的代码性能优化示例
现在有一种情况,容器可以放一件东西。容器支持create方法来创建新事物并将其放入容器中,并支持get方法来获取事物在容器中。我们可以轻松地编写以下代码。
1 public class Container { 2 public static class SomeThing { 3 private int status; 4 5 public SomeThing() { 6 status = 1; 7 } 8 9 public int getStatus() { 10 return status; 11 } 12 } 13 14 private SomeThing object; 15 16 public void create() { 17 object = new SomeThing(); 18 } 19 20 public SomeThing get() { 21 while (object == null) { 22 Thread.yield(); //不加这句话可能会在此出现无限循环 23 } 24 return object; 25 } 26 }
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-354009-1.html
再见
没有大陆撑腰
还不全是为了石油