写点什么

浅析 Java 内存模型 一

用户头像
朱华
关注
发布于: 2020 年 10 月 10 日
浅析 Java 内存模型 一

JVM 内存结构、Java 内存模型和 Java 对象模型



JVM 内存结构

在 JDK1.8 的时候,JVM内存模型直接将方法区移到了本地内存中,叫元数据空间。该区域的内存大小就只受本机总内存的限制,但是当申请不到足够内存时也会报错。



  • 堆(线程共享)

  • 虚拟机栈(线程私有)

  • 方法区(线程共享)

  • 本地方法栈(线程私有)

  • 程序计数器(线程私有)



Java 对象模型



一个 Java 对象可以分为三部分存储在内存中,分别是:



  1. 对象头(包含锁状态标志,线程持有的锁等标志)

  2. 实例数据

  3. 对齐填充



OOP-Klass model(hotspot jvm 中的对象模型)



Java 虚拟机的底层是使用 C++ 实现的,而 JVM 并没有根据一个 Java 的实例对象去创建对应的 C++ 对象,而是设计了一个 oop-klass model

  1. OOP(Ordinary Object Pointer):普通对象指针,表示一个实例信息。

  2. Klass:描述对象实例的具体类型,包含元数据和方法信息。

  3. 创建目的:不想让每个对象中都含有一个 vtable(虚函数表)。



整体方向:

  • JVM 内存结构,和 Java 虚拟机的运行时数据区有关。

  • Java 对象模型,和 Java 对象在虚拟机中的表现形式有关。

  • Java 内存模型,和 Java 的并发编程有关。



Java 内存模型

《Java虚拟机规范》中曾试图定义一种“Java 内存模型”(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如 C 和 C++ 等)直接使用物理硬件和操作系统的内存模型。因此,由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,所以在某些场景下必须针对不同的平台来编写程序。



Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。但是在此之前我们先了解一下物理计算机中的并发问题。



缓存一致性





想要保证每个处理器能在任意时间内获知其他处理器正在进行的工作,其代价非常高昂。大多数时间里这些信息都是没用的,所以处理器会牺牲存储一致性的保证,来换取性能的提升。



一种架构的存储模型告诉了应用程序可以从它的存储系统中获得何种担保,同时详细定义了一些特殊的指令称为存储关卡( memory barriers)或栏( fences),用以在需要共享数据时,得到额外的存储协调保证。为了帮助Java开发者屏蔽这些跨架构的存储模型之间的不同,Java提供了自己的存储模型,JⅥM会通过在适当的位置上插入存储关卡,来解决JMM与底层平台存储模型之间的差异化。



总结: JMM 是什么

  • 是一组规范,需要各个 JVM 的实现来遵守 JMM 规范,一遍开发者可以利用这些规范,更方便地开发多线程程序。

  • 如果没有这样的一个 JMM 内存模型来规范,那么很可能经过了不同 JVM 的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样那是很大的问题。

  • volatile、synchronized、Lock 等的原理都是 JMM。

  • 如果没有 JMM,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了 JMM,让我们只需要用同步工具和关键字就可以开发并发程序。



有序性

Java 程序中天然的有序性可以 总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的。前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(3);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
two.start();
one.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 1 && y == 1) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}



重排序分析

  • 根据两个线程的执行顺序,分析这 4 行代码的执行决定了最终 x、y 的结果,一共有 3 中情况:

  • 虽然代码执行顺序可能有多重情况,但是在线程 1 内部,也就是:a=1; x=b;

  • 会出现 x=0, y=0 的现象?那是因为重排序发生了,4 行代码的执行顺序的其中一种可能:



什么是重排序

在线程 1 内部的两行代码的实际执行顺序和代码在 Java 文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序。



对比重排序前后的指令优化





重排序的 3 种情况

  • 编译器优化:包括 JVM,JIT 编译器等

  • CPU 指令重排

  • 内存的“重排序”:线程 A 的修改,线程 B 缺看不到,引出可见性问题



可见性

public class FieldVisibility {
volatile int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("a=" + a + ";b=" + b);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}

分析这 4 种情况





  1. a=1, b=2

  2. a=3, b=3

  3. a=3, b=2

  4. a=1, b=3可见性问题



什么是可见性问题



public class FieldVisibility2 {
int x = 0;
public void writeThread() {
x = 1;
}
public void readerThread() {
int r2 = x;
}
}



利用 volatile 解决问题





为什么会有可见性问题

  1. CPU 缓存结构图

  1. CPU 有多级缓存,导致读的数据过期

高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在 CPU 和主内存之间就多了 Cache 层

线程间的对于共享变量的可见性问题不是直接有多核引起的,而是由多缓存引起的。

如果所有的核心都只用一个缓存,那么也就不存在内存可见性问题了。

每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中,所以会导致有些核心读取的值是一个过期的值。



主内存和本地内存

Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图 12- 2 所示,注意与图 12-1 进行对比。





主内存和本地内存的关系

  1. 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存工作内存中的变量内容是主内存中的拷贝

  2. 线程不能直接读写主内存中的变量而是只能操作自己工作內存中的变量然后再同步到主内存中

  3. 主内存多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成



所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题



发布于: 2020 年 10 月 10 日阅读数: 64
用户头像

朱华

关注

见自己,见天地,见众生。 2018.08.07 加入

还未添加个人简介

评论

发布
暂无评论
浅析 Java 内存模型 一