# 引入

内存模型和内存结构不一样,通过本节从堆栈角度引入 JMM,为后续内容做铺垫。

JMM 在线程栈和堆之间划分内存,如果你系统的学习过 Java,一定听过引用保存在栈中,实例化对象保存在堆中这样的话。从逻辑上说明 JMM:

由线程创建的局部变量对于创建它的线程以外的所有其他线程是不可见的。 即使两个线程正在执行完全相同的代码,两个线程仍将在每个自己的线程堆栈中创建该代码的局部变量。

基本类型的局部变量完全存储在线程堆栈中,对其他线程不可见;但是 new 对象时,都存储在堆上,这个对象的成员变量与对象一起存储在堆上。

# JMM 与硬件内存结构关系

其实该部分在 J.U.Cvolatile 关键字讲过。

先看内存结构图:

内存->cache->寄存器 :有些结构是将 cache 也到了 CPU 里面。

因为 JVM 也是一个运行在 CPU 的程序,所以堆内存和线程栈的数据可能在三个位置,就会导致可见性和同步性的问题:

可见性:对象共享后,某一个线程做出修改,但是还没有刷新到主存中,就会导致另一个线程看不到修改。

同步性:多个线程的修改,后一个线程可能会掩盖前一个线程的修改。

这些都是并发讲过的,不多说了,前一个问题通过 volatile 解决,后一个问题通过上锁解决。

关于第二个问题,同步块有个特性:同步块还保证在同步块内访问的所有变量都将从主存储器中读入,当线程退出同步块时,所有更新的变量将再次刷新回主存储器,无论变量是不是声明为 volatile。

# 内存模型

接下来的内容与并发内容多有相似之处。总结于 Info 上的深入理解 Java 内存模型,作者程晓明。

# 基础

线程之间通信机制有两种:共享内存和消息传递。前者很简单,后者的模型中,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

# JMM 抽象

在引入中就讲到 JMM,线程之间内存不共享。Java 中,所有实例,静态域,数组元素存储在堆内存中。我们常说的线程通信,其实就是通过主内存进行通信,因为每个线程都有副本,所以会出现可见性问题。

happens-before:这个概念用来阐述操作之间的内存可见性,如果 a 操作的结果需要对 b 操作可见,那么这两个操作必须存在 happens-before 关系。

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

  • volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。

  • 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

因为内容完整性才写的这个,感觉都没什么好说的,知道有这么个概念即可。

# 重排序

数据依赖性:如果两个指令(操作)重排序会导致执行结果改变,就不允许重排序:写后读,读后写,写后写。

编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

as-if-serial:不管怎么重排序,(单线程)程序的执行结果不能被改变。

double pi = 3.14; //A
double r   = 1.0; //B
double area = pi * r * r; //C

三个操作数依赖关系:

其实就是 A,B 可以随意交换执行顺序。其实在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。编译器和处理器遵从这一目标,从 happens- before 的定义我们可以看出,JMM 同样遵从这一目标。

# 总结

个人感觉:JMM 就是为了(多)线程指令正确执行而做出的努力。

一般的处理器内存模型都符合这么一个规律:越是追求性能的处理器,内存模型设计会越弱,因为处理器希望内存模型对他们的束缚越少越好。毕竟为了保证并发环境下程序正确执行,会消耗大量资源。

JMM 是一个语言级的内存模型,处理器内存模型是硬件级的内存模型

不同处理器平台对于指令的重排序也不相同,JMM 通过插入内存屏障阻止这些不同处理器的不同重排序,向上屏蔽了处理器内存模型的差异,它在不同的处理器平台之上为 java 程序员呈现了一个一致的内存模型。

# JMM 的设计

从 JMM 设计者的角度来说,在设计 JMM 时,需要考虑两个关键因素:

  • 程序员对内存模型的使用。程序员希望内存模型易于理解,易于编程。程序员希望基于一个强内存模型来编写代码。
  • 编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。

一方面要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能的放松。

下面让我们看看 JSR-133 是如何实现这一目标的。

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C
  • A happens- before B;
  • B happens- before C;
  • A happens- before C;

happens- before 的定义会要求:A 操作执行的结果要对 B 可见,且 A 操作的执行顺序排在 B 操作之前。 但是从程序语义的角度来说,对 A 和 B 做重排序即不会改变程序的执行结果,也还能提高程序的执行性能(允许这种重排序减少了对编译器和处理器优化的束缚)。也就是说,上面这 3 个 happens- before 关系中,虽然 2 和 3 是必需要的,但 1 是不必要的。因此,JMM 把 happens- before 要求禁止的重排序分为了下面两类:

  • 会改变程序执行结果的重排序。
  • 不会改变程序执行结果的重排序。

这里说的 happens- before 要求禁止的重排序就是 A happens-before B,也就是 A 的结果 B 必须可见(但是显然这不是必须的)。

JMM 对这两种不同性质的重排序,采取了不同的策略:

  • 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM 对编译器和处理器不作要求(JMM 允许这种重排序)。

第二种就是,JMM 放养了,随便编译器和处理器是否排序。

# 参考

https://pdai.tech/md/java/jvm/java-jvm-jmm.html

https://www.infoq.cn/minibook/java_memory_model