本文我们将重点放在 Java 内存模型( JMM )的一些高层设计问题,以及 JMM 的底层需求和所提供的保证,还有一些高层设计原则背后的原理。
例如安全发布,同步策略的规范以及一致性等。他们的安全性都来自于 JMM ,并且当你理解了这些机制的工作原理后,就能更容易的使用他们。
1 、什么是内存模型,为什么要使用它
假设一个线程为变量 aVar 赋值:
a = 3;
内存模型要解决的问题是: “ 在什么条件下,读取 a 的线程可以看到这个值为 3 ? ” 。这听起来似乎是一个愚蠢的问题,但如果缺少同步,那么会有很多因素导致无法立即、甚至永远看不到一个线程的操作结果。这包括很多因素,如果没有使用正确的同步,例如:
- 编译器中生成的指令顺序与源代码中的顺序不同;
- 编译器将变量保存在寄存器而不是内存中;
- 处理器可以乱序或者并行执行指令;
- 缓存可能会改变将写入变量提交到主内存的次序;
- 处理器中也有本地缓存,对其他处理器不可见;
在单线程中,我们无法看到所有这些底层技术,他们除了提高成勋的执行速度,不会产生其他影响。 Java 语言规范要求 JVM 在线程中维护一种类似串行的语义:只要程序的最终结果与在严格环境中的执行结果相同,那么上述操作都是允许的。
这确实是一件好事情,因为在近几年中,计算性能的提升在很大的程度上要归功于:
- 重新排序措施;
- 时钟频率的提升;
- 不断提升的并行性;
- 采用流水线的超标量执行单元,动态指令调整, 猜测执行以及完备的多级缓存等;
随着处理器越来越强大,编译器也在不断的改进,通过指令重排序实现优化执行,以及使用成熟的全局寄存器分配算法。由于时钟频率越来越难以提高,因此许多处理器生产商都开始转而生产多核处理器,因为能够提高的只有硬件的并行性。
在多线程环境中,维护程序的串行性将导致很大的性能开销,并发程序中的线程,大多数时间里都执行各自的任务,因此线程之间协调操作只会降低应用程序的运行速度,不会带来任何好处。只有当多个线程要共享数据时,才必须协调他们之间的操作,并且 JVM 依赖程序通过同步操作找出这些协调操作将何时发生。
JMM 规定了 JVM 必须遵循一组最小的保证,这组保证规定了对变量的写入操作在何时将对其他线程可见。
JMM 在设计时就在可预测性与易于开发性之间进行了权衡,从而在各种主流的处理器体系架构上能实现高性能的 JVM 。如果你不了解在现代处理器和编译器中使用的程序性能提升措施,那么在刚刚接触 JMM 的某些方面时会感到困惑。
1.1 平台的内存模型
在共享内存的多处理器体系架构中,每个处理器拥有自己的缓存,并且定期的与主内存进行协调。在不同的处理器架构中提供了不同级别的缓存一致性( cache coherence )。其中一部分只提供最小的保证,即允许不同的处理器在任意时刻从同一个存储位置上看到不同的值。操作系统、编译器以及 runtime 运行时(有时甚至包括应用程序)需要弥补这种硬件能力与线程安全需求之间的差异。
要确保每个处理器在任意时刻都知道其他处理器在进行的工作,这将开销巨大。多数情况下,这完全没必要,可随意放宽存储一致性,换取性能的提升。
在架构定义的内存模型中将告诉应用程序可以从内存系统中获取怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证。为了使 Java 开发人员无须关心不同架构上内存模型之间的差异, Java 还提供了自己的内存