写点什么

【深入浅出 JVM 原理及调优】「搭建理论知识框架」全方位带你探索和分析 JMM 并发模型之(重排序机制)

作者:洛神灬殇
  • 2024-01-11
    江苏
  • 本文字数:7669 字

    阅读完需:约 25 分钟

【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你探索和分析JMM并发模型之(重排序机制)

专栏介绍

学习 JVM 需要一定的编程经验和计算机基础知识,适用于从事 Java 开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。

前提准备

  • 编程基础:具备良好的编程基础,理解面向对象编程(OOP)的基本概念,熟悉 Java 编程语言。

  • 数据结构与算法:对基本的数据结构和算法有一定了解,理解内存管理、线程操作等基本概念。

面向人群

学习本专栏以及本章内容的前提和适用人群如下:

  • Java 开发人员:JVM 是 Java 程序的核心执行引擎,因此 Java 开发人员需要深入了解 JVM 的工作原理和运行机制,以优化程序性能并解决相关问题。

  • 系统架构师和高级工程师:对系统整体性能、稳定性有较高要求的人群,有必要深入理解 JVM 以优化系统性能。

  • Java 程序员和技术爱好者:具备一定 Java 编程经验,有意向深入了解 JVM 内部工作原理的人群。

  • 研究人员和学生:从事计算机科学相关研究或学习的人群,有兴趣深入研究 JVM 内部原理和优化方法。

  • JVM 运维工程师:负责 JVM 性能优化、故障排查和调优的专业人员,需要对 JVM 有深入的理解。

知识脉络

每位 Java 开发者都了解到 Java 字节码是在 Java 运行时环境(JRE)上执行的。JRE 包含了最为关键的组成部分:Java 虚拟机(JVM),它负责分析和执行 Java 字节码。通常情况下,大多数 Java 开发者无需深入了解虚拟机的内部运行原理。即使对虚拟机的运行机制不甚了解,也不会对开发工作产生太多影响。然而,对 JVM 有一定了解的话,将更有助于深入理解 Java 语言,并解决一些看似困难的问题。

本专栏全面系统地剖析了特定虚拟机产品(即 HotSpot,Oracle 官方虚拟机)的实现,本人不仅深刻地讲解了看似深奥的原理,还提供了大量易于上手的实践案例,下面是总体的 JVM 相关的知识拓扑架构。

添加图片注释,不超过 140 字(可选)

tips:当然还有一些最新的 JVM 特性未在这张图并非展示本专栏的全部内容,另外还包含了最新的 JVM 特性。


重排序

在计算机领域,软件技术和硬件技术共同的目标是在不改变程序执行结果的前提下尽可能地提高并行度。编译器和处理器都致力于实现这一目标,并且 Java 内存模型(JMM)也遵循这一原则。

数据依赖性

当两个操作访问同一个变量时,并且其中至少一个操作是写操作,这两个操作之间就存在数据依赖关系。数据依赖关系可以被分为以下三种类型:

添加图片注释,不超过 140 字(可选)

读后写依赖(Read-After-Write Dependency)

当一个写操作在一个读操作之后发生时,存在读后写依赖。换句话说,后续的读操作在之前的写操作完成之后才能获取到最新的数据。

// 读一个变量之后,再写这个变量。a=b;b=1;
复制代码

写后读依赖(Write-After-Read Dependency)

当一个读操作在一个写操作之后发生时,存在写后读依赖。这意味着读操作获取的是在之前写操作完成时的旧数据,而不是最新的数据。

代码案例

//写一个变量之后,再读这个位置。a=1;b=a;
复制代码

写后写依赖(Write-After-Write Dependency)

当两个或多个写操作在彼此之间发生时,存在写后写依赖。这意味着后续的写操作依赖于之前的写操作的结果。

代码案例

// 写一个变量之后,再写这个变量。a=1;a=2;
复制代码

如果对上面的操作进行重排序,可能会导致程序的执行结果发生变化。编译器和处理器会遵守数据依赖性原则,不会改变具有数据依赖关系的两个操作的执行顺序,以确保结果的正确性。

注意,上述讨论的数据依赖性仅适用于在单个处理器中执行的指令序列或单个线程中执行的操作。编译器和处理器不考虑不同处理器之间或不同线程之间的数据依赖性。因此,为确保多线程程序的正确性,需要使用同步机制,如锁、原子操作等,以保证数据的正确读取和写入。这样可以解决不同线程之间的并发冲突和重排序问题,确保程序的正确执行。

as-if-serial 语义

as-if-serial 语义指的是无论编译器和处理器如何进行重排序,单线程程序的执行结果不能被改变。

as-if-serial 的问题和局限性

编译器、运行时环境和处理器都必须遵守 as-if-serial 语义,这意味着编译器、运行时环境和处理器可以对操作进行重排序,只要其不改变程序在单线程执行下的结果。通过这种优化,可以提高并行性和性能。

as-if-serial 语义确保对于单线程程序来说,其执行结果始终与按照顺序执行所有操作的结果一致。

double pi = 3.14;  //Adouble r = 1.0; //Bdouble area = pi *r * r; //C
复制代码

上面三个操作的数据依赖关系如下图所示:

添加图片注释,不超过 140 字(可选)

依赖关系分析

在上述图示中,表明了 A 和 C 之间存在数据依赖关系,并且 B 和 C 之间也存在数据依赖关系。这确保了在最终的指令序列中,C 不能被重排序到 A 和 B 的前面。这样做是为了确保程序的结果不会被改变。

A 和 B 之间并没有数据依赖关系,这意味着在编译器和处理器的优化下,A 和 B 之间的执行顺序可以重排序。这种重排序不会影响程序的结果,因为它们之间没有数据依赖关系。

在指令序列的执行中,可以出现两种不同的执行顺序,而结果是等效的。这允许编译器和处理器在没有改变程序语义的情况下进行优化,提高并行性和性能。

两种执行顺序场景

添加图片注释,不超过 140 字(可选)

按程序顺序的执行结果:area=3.14,重排序后的执行结果:area=3.14。

程序顺序规则

根据 happens-before 的程序顺序规则,上面计算圆的面积的示例代码存在三个 happens-before 关系:

对于第三个 happens-before 关系,根据 happens-before 的传递性,可以推导出 A happens-before B。

添加图片注释,不超过 140 字(可选)

尽管 A happens-before B,根据之前提到的重排序后的执行顺序,B 可以在 A 之前执行。在 Java 内存模型(JMM)中,happens-before 关系并不要求 A 一定要在 B 之前执行。JMM 只要求前一个操作(执行结果)对后一个操作可见,并且前一个操作按顺序排在第二个操作之前。

在这种情况下,操作 A 的执行结果不需要对操作 B 可见。而且,重排序操作 A 和操作 B 后的执行结果与按照 happens-before 顺序执行操作 A 和操作 B 的结果是一致的。因此,JMM 认为这种重排序是合法的,它允许这种重排序的优化。

注意,JMM 的目的是确保多线程程序的正确性和一致性,而不是强制要求按照规定的顺序执行。JMM 允许对指令进行适当的重排序以提高性能和并发度,同时确保程序的语义和结果与顺序执行的情况一致。

重排序对多线程的影响

现在让我们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:

class ReorderExample {int a 0;boolean flag false;public void writer(){  a=1; //1  flag = true; //2}Public void reader({  if (flag) //3   int i=a*a; //4}
复制代码

flag 变量是个标记, 用来标识变量 a 是否已被写入。这里假设有两个线程 A 和 B,A 首先执行 writer 方法, 随后 B 线程接着执行 reader 方法。线程 B 在执行操作 4 时,不一定能看到线程 A 在操作 1 对共享变量 a 的写入结果。

操作 1 和操作 2 的重排序问题分析

由于操作 1 和操作 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作 1 和操作 2 重排序时,可能会产生什么效果,请看下面的程序执行时序图:

添加图片注释,不超过 140 字(可选)

如上图所示,操作 1 和操作 2 做了重排序。程序执行时,线程 A 首先写标记变量 flag, 随后线程 B 读这个变量。由于条件判断为真, 线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

操作 3 和操作 4 的重排序问题分析

下面再让我们看看,当操作 3 和操作 4 重排序时会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)。下面是操作 3 和操作 4 重排序后,程序的执行时序图:

添加图片注释,不超过 140 字(可选)

操作 3 和操作 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此, 编译器和处理器会采用猜测(Speculation) 执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB) 的硬件缓存中。当接下来操作 3 的条件判断为真时, 就把该计算结果写入变量 i 中。

从图中我们可以看出,猜测执行实质上对操作 3 和 4 做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as-if-serial 语义允许对存在控制依赖的操作做重排序的原因) ; 但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

总结分析

As-if-serial

数据依赖关系对于确保程序结果的准确性至关重要,但是在没有数据依赖关系的情况下,编译器和处理器可以对指令重排序以优化性能,并且仍然必须遵循程序的 as-if-serial 语义。

As-if-serial 规则确保在单线程环境下,重排序后的指令序列的结果与按顺序执行所有操作的结果一致。这意味着重排序不能改变单线程程序的执行结果。

此外,as-if-serial 语义还解决了内存可见性问题。编译器,并且处理器在执行指令的过程中,会确保在多个线程之间正确地处理共享内存的读写操作,以保证单线程程序的数据一致性。

Happens-Before

根据 happens-before 关系的定义,我们可以看出 JMM 也遵循这一目标。happens-before 关系确保了在多线程环境中,编写的程序的顺序和语义与按序执行的情况一致,但并不限制编译器和处理器对指令进行优化和重排序。

编译器和处理器会根据 happens-before 关系和 as-if-serial 语义,对指令进行优化和重排序,以提高并行性和性能,同时确保程序的执行结果与顺序执行的结果一致。

发布设置

添加封面

图片上传格式支持 JPEG、JPG、PNG

创作声明​无声明

*

文章话题​Java 虚拟机(JVM)Java 并发

内容来源根据自媒体投稿规范,发布新闻类的内容需要明确标注信息来源,请确认当前内容是否已正确添加​添加来源

发布设置

字数:4111

Markdown 语法识别中​

草稿已保存

预览

更新

历史版本

2024-01-08 19:51当前草稿2023-12-06 22:40发布过的版本2023-12-06 21:49自动保存2023-12-06 21:28自动保存2023-12-06 21:02自动保存

【深入浅出 JVM 原理及调优】「搭建理论知识框架」全方位带你探索和分析 JMM 并发模型之(重排序机制)

专栏介绍

学习 JVM 需要一定的编程经验和计算机基础知识,适用于从事 Java 开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。

前提准备

  • 编程基础:具备良好的编程基础,理解面向对象编程(OOP)的基本概念,熟悉 Java 编程语言。

  • 数据结构与算法:对基本的数据结构和算法有一定了解,理解内存管理、线程操作等基本概念。

面向人群

学习本专栏以及本章内容的前提和适用人群如下:

  • Java 开发人员:JVM 是 Java 程序的核心执行引擎,因此 Java 开发人员需要深入了解 JVM 的工作原理和运行机制,以优化程序性能并解决相关问题。

  • 系统架构师和高级工程师:对系统整体性能、稳定性有较高要求的人群,有必要深入理解 JVM 以优化系统性能。

  • Java 程序员和技术爱好者:具备一定 Java 编程经验,有意向深入了解 JVM 内部工作原理的人群。

  • 研究人员和学生:从事计算机科学相关研究或学习的人群,有兴趣深入研究 JVM 内部原理和优化方法。

  • JVM 运维工程师:负责 JVM 性能优化、故障排查和调优的专业人员,需要对 JVM 有深入的理解。

知识脉络

每位 Java 开发者都了解到 Java 字节码是在 Java 运行时环境(JRE)上执行的。JRE 包含了最为关键的组成部分:Java 虚拟机(JVM),它负责分析和执行 Java 字节码。通常情况下,大多数 Java 开发者无需深入了解虚拟机的内部运行原理。即使对虚拟机的运行机制不甚了解,也不会对开发工作产生太多影响。然而,对 JVM 有一定了解的话,将更有助于深入理解 Java 语言,并解决一些看似困难的问题。

本专栏全面系统地剖析了特定虚拟机产品(即 HotSpot,Oracle 官方虚拟机)的实现,本人不仅深刻地讲解了看似深奥的原理,还提供了大量易于上手的实践案例,下面是总体的 JVM 相关的知识拓扑架构。



tips:当然还有一些最新的 JVM 特性未在这张图并非展示本专栏的全部内容,另外还包含了最新的 JVM 特性。


重排序

在计算机领域,软件技术和硬件技术共同的目标是在不改变程序执行结果的前提下尽可能地提高并行度。编译器和处理器都致力于实现这一目标,并且 Java 内存模型(JMM)也遵循这一原则。

数据依赖性

当两个操作访问同一个变量时,并且其中至少一个操作是写操作,这两个操作之间就存在数据依赖关系。数据依赖关系可以被分为以下三种类型:

读后写依赖(Read-After-Write Dependency)

当一个写操作在一个读操作之后发生时,存在读后写依赖。换句话说,后续的读操作在之前的写操作完成之后才能获取到最新的数据。

// 读一个变量之后,再写这个变量。a=b;b=1;
复制代码

写后读依赖(Write-After-Read Dependency)

当一个读操作在一个写操作之后发生时,存在写后读依赖。这意味着读操作获取的是在之前写操作完成时的旧数据,而不是最新的数据。

代码案例

//写一个变量之后,再读这个位置。a=1;b=a;
复制代码

写后写依赖(Write-After-Write Dependency)

当两个或多个写操作在彼此之间发生时,存在写后写依赖。这意味着后续的写操作依赖于之前的写操作的结果。

代码案例

// 写一个变量之后,再写这个变量。a=1;a=2;
复制代码

如果对上面的操作进行重排序,可能会导致程序的执行结果发生变化。编译器和处理器会遵守数据依赖性原则,不会改变具有数据依赖关系的两个操作的执行顺序,以确保结果的正确性。

注意,上述讨论的数据依赖性仅适用于在单个处理器中执行的指令序列或单个线程中执行的操作。编译器和处理器不考虑不同处理器之间或不同线程之间的数据依赖性。因此,为确保多线程程序的正确性,需要使用同步机制,如锁、原子操作等,以保证数据的正确读取和写入。这样可以解决不同线程之间的并发冲突和重排序问题,确保程序的正确执行。

as-if-serial 语义

as-if-serial 语义指的是无论编译器和处理器如何进行重排序,单线程程序的执行结果不能被改变。

as-if-serial 的问题和局限性

编译器、运行时环境和处理器都必须遵守 as-if-serial 语义,这意味着编译器、运行时环境和处理器可以对操作进行重排序,只要其不改变程序在单线程执行下的结果。通过这种优化,可以提高并行性和性能。

as-if-serial 语义确保对于单线程程序来说,其执行结果始终与按照顺序执行所有操作的结果一致。

double pi = 3.14;  //Adouble r = 1.0; //Bdouble area = pi *r * r; //C
复制代码

上面三个操作的数据依赖关系如下图所示:

依赖关系分析

在上述图示中,表明了 A 和 C 之间存在数据依赖关系,并且 B 和 C 之间也存在数据依赖关系。这确保了在最终的指令序列中,C 不能被重排序到 A 和 B 的前面。这样做是为了确保程序的结果不会被改变。

A 和 B 之间并没有数据依赖关系,这意味着在编译器和处理器的优化下,A 和 B 之间的执行顺序可以重排序。这种重排序不会影响程序的结果,因为它们之间没有数据依赖关系。

在指令序列的执行中,可以出现两种不同的执行顺序,而结果是等效的。这允许编译器和处理器在没有改变程序语义的情况下进行优化,提高并行性和性能。

两种执行顺序场景


按程序顺序的执行结果:area=3.14,重排序后的执行结果:area=3.14。

程序顺序规则

根据 happens-before 的程序顺序规则,上面计算圆的面积的示例代码存在三个 happens-before 关系:

对于第三个 happens-before 关系,根据 happens-before 的传递性,可以推导出 A happens-before B。

尽管 A happens-before B,根据之前提到的重排序后的执行顺序,B 可以在 A 之前执行。在 Java 内存模型(JMM)中,happens-before 关系并不要求 A 一定要在 B 之前执行。JMM 只要求前一个操作(执行结果)对后一个操作可见,并且前一个操作按顺序排在第二个操作之前。

在这种情况下,操作 A 的执行结果不需要对操作 B 可见。而且,重排序操作 A 和操作 B 后的执行结果与按照 happens-before 顺序执行操作 A 和操作 B 的结果是一致的。因此,JMM 认为这种重排序是合法的,它允许这种重排序的优化。

注意,JMM 的目的是确保多线程程序的正确性和一致性,而不是强制要求按照规定的顺序执行。JMM 允许对指令进行适当的重排序以提高性能和并发度,同时确保程序的语义和结果与顺序执行的情况一致。

重排序对多线程的影响

现在让我们来看看,重排序是否会改变多线程程序的执行结果。请看下面的示例代码:

class ReorderExample {int a 0;boolean flag false;public void writer(){  a=1; //1  flag = true; //2}Public void reader({  if (flag) //3   int i=a*a; //4}
复制代码

flag 变量是个标记, 用来标识变量 a 是否已被写入。这里假设有两个线程 A 和 B,A 首先执行 writer 方法, 随后 B 线程接着执行 reader 方法。线程 B 在执行操作 4 时,不一定能看到线程 A 在操作 1 对共享变量 a 的写入结果。

操作 1 和操作 2 的重排序问题分析

由于操作 1 和操作 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作 1 和操作 2 重排序时,可能会产生什么效果,请看下面的程序执行时序图:

如上图所示,操作 1 和操作 2 做了重排序。程序执行时,线程 A 首先写标记变量 flag, 随后线程 B 读这个变量。由于条件判断为真, 线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

操作 3 和操作 4 的重排序问题分析

下面再让我们看看,当操作 3 和操作 4 重排序时会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)。下面是操作 3 和操作 4 重排序后,程序的执行时序图:

操作 3 和操作 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此, 编译器和处理器会采用猜测(Speculation) 执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB) 的硬件缓存中。当接下来操作 3 的条件判断为真时, 就把该计算结果写入变量 i 中。

从图中我们可以看出,猜测执行实质上对操作 3 和 4 做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as-if-serial 语义允许对存在控制依赖的操作做重排序的原因) ; 但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

总结分析

As-if-serial

数据依赖关系对于确保程序结果的准确性至关重要,但是在没有数据依赖关系的情况下,编译器和处理器可以对指令重排序以优化性能,并且仍然必须遵循程序的 as-if-serial 语义。

As-if-serial 规则确保在单线程环境下,重排序后的指令序列的结果与按顺序执行所有操作的结果一致。这意味着重排序不能改变单线程程序的执行结果。

此外,as-if-serial 语义还解决了内存可见性问题。编译器,并且处理器在执行指令的过程中,会确保在多个线程之间正确地处理共享内存的读写操作,以保证单线程程序的数据一致性。

Happens-Before

根据 happens-before 关系的定义,我们可以看出 JMM 也遵循这一目标。happens-before 关系确保了在多线程环境中,编写的程序的顺序和语义与按序执行的情况一致,但并不限制编译器和处理器对指令进行优化和重排序。

编译器和处理器会根据 happens-before 关系和 as-if-serial 语义,对指令进行优化和重排序,以提高并行性和性能,同时确保程序的执行结果与顺序执行的结果一致。

2024-01-08 19:51 当前草稿

恢复此版本

发布于: 刚刚阅读数: 4
用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

👑 后端技术架构师,前优酷资深工程师 📕 个人著作《深入浅出Java虚拟机—JVM原理与实战》 💻 10年开发经验,参与过多个大型互联网项目,定期分享技术干货和项目经验

评论

发布
暂无评论
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你探索和分析JMM并发模型之(重排序机制)_Java_洛神灬殇_InfoQ写作社区