学习JVM需要一定的编程经验和计算机基础知识,适用于从事Java开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。
学习本专栏以及本章内容的前提和适用人群如下:
每位Java开发者都了解到Java字节码是在Java运行时环境(JRE)上执行的。JRE包含了最为关键的组成部分:Java虚拟机(JVM),它负责分析和执行Java字节码。通常情况下,大多数Java开发者无需深入了解虚拟机的内部运行原理。即使对虚拟机的运行机制不甚了解,也不会对开发工作产生太多影响。然而,对JVM有一定了解的话,将更有助于深入理解Java语言,并解决一些看似困难的问题。
本专栏全面系统地剖析了特定虚拟机产品(即HotSpot,Oracle官方虚拟机)的实现,本人不仅深刻地讲解了看似深奥的原理,还提供了大量易于上手的实践案例,下面是总体的JVM相关的知识拓扑架构。
tips:当然还有一些最新的JVM特性未在这张图并非展示本专栏的全部内容,另外还包含了最新的JVM特性。
在计算机领域,软件技术和硬件技术共同的目标是在不改变程序执行结果的前提下尽可能地提高并行度。编译器和处理器都致力于实现这一目标,并且Java内存模型(JMM)也遵循这一原则。
当两个操作访问同一个变量时,并且其中至少一个操作是写操作,这两个操作之间就存在数据依赖关系。数据依赖关系可以被分为以下三种类型:
当一个写操作在一个读操作之后发生时,存在读后写依赖。换句话说,后续的读操作在之前的写操作完成之后才能获取到最新的数据。
// 读一个变量之后,再写这个变量。
a=b;
b=1;
当一个读操作在一个写操作之后发生时,存在写后读依赖。这意味着读操作获取的是在之前写操作完成时的旧数据,而不是最新的数据。
//写一个变量之后,再读这个位置。
a=1;
b=a;
当两个或多个写操作在彼此之间发生时,存在写后写依赖。这意味着后续的写操作依赖于之前的写操作的结果。
// 写一个变量之后,再写这个变量。
a=1;
a=2;
如果对上面的操作进行重排序,可能会导致程序的执行结果发生变化。编译器和处理器会遵守数据依赖性原则,不会改变具有数据依赖关系的两个操作的执行顺序,以确保结果的正确性。
注意,上述讨论的数据依赖性仅适用于在单个处理器中执行的指令序列或单个线程中执行的操作。编译器和处理器不考虑不同处理器之间或不同线程之间的数据依赖性。因此,为确保多线程程序的正确性,需要使用同步机制,如锁、原子操作等,以保证数据的正确读取和写入。这样可以解决不同线程之间的并发冲突和重排序问题,确保程序的正确执行。
as-if-serial语义指的是无论编译器和处理器如何进行重排序,单线程程序的执行结果不能被改变。
编译器、运行时环境和处理器都必须遵守as-if-serial语义,这意味着编译器、运行时环境和处理器可以对操作进行重排序,只要其不改变程序在单线程执行下的结果。通过这种优化,可以提高并行性和性能。
as-if-serial语义确保对于单线程程序来说,其执行结果始终与按照顺序执行所有操作的结果一致。
double pi = 3.14; //A
double r = 1.0; //B
double 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没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作1和操作2重排序时,可能会产生什么效果,请看下面的程序执行时序图:
如上图所示,操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag, 随后线程B读这个变量。由于条件判断为真, 线程B将读取变量a。此时,变量a还根本没有被线程A写入,在这里多线程程序的语义被重排序破坏了!
下面再让我们看看,当操作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语义还解决了内存可见性问题。编译器,并且处理器在执行指令的过程中,会确保在多个线程之间正确地处理共享内存的读写操作,以保证单线程程序的数据一致性。
根据happens-before关系的定义,我们可以看出JMM也遵循这一目标。happens-before关系确保了在多线程环境中,编写的程序的顺序和语义与按序执行的情况一致,但并不限制编译器和处理器对指令进行优化和重排序。
编译器和处理器会根据happens-before关系和as-if-serial语义,对指令进行优化和重排序,以提高并行性和性能,同时确保程序的执行结果与顺序执行的结果一致。