Java Memory Model

简介

在多处理器系统中,处理器通常有一个或多个内存缓存层,通过加速对数据的访问(因为数据离处理器更近)和减少共享内存总线上的流量(因为许多内存操作可以由本地缓存来满足),提高性能。内存缓存可以极大地提高性能,但是它们带来了许多新的挑战。

例如,当两个处理器同时检查相同的内存位置时,会发生什么?在什么条件下它们会看到相同的值?

在处理器级别,内存模型定义了必要和充分的条件,以便知道其他处理器对内存的写操作对当前处理器是可见的,而当前处理器对其他处理器的写操作是可见的。

有些处理器显示了强内存模型,在这种模型中,所有处理器在任何给定的内存位置上看到的值都是完全相同的。

其他处理器表现出较弱的内存模型,其中需要特殊指令(称为内存屏障)来刷新或使本地处理器缓存无效,以查看其他处理器所做的写操作,或使此处理器的写操作对其他人可见。

这些内存屏障通常在采取锁和解锁动作时执行;在高级语言中,它们是不可见的。

有时为强内存模型编写程序更容易,因为内存障碍的需求减少了。

然而,即使在一些最强大的记忆模型中,记忆障碍也常常是必要的;他们的位置常常是违反直觉的。

处理器设计的最新趋势鼓励了较弱的内存模型,因为它们对缓存一致性的放宽允许跨多个处理器和更大的内存量有更大的可伸缩性。

当写入对另一个线程可见时,编译器对代码的重新排序会加剧这个问题。

例如,编译器可能认为在程序的后面移动写操作更有效;只要这个代码动作不改变程序的语义,它就可以自由地这么做。

如果编译器对操作进行了修改,那么在执行之前,另一个线程将不会看到它;这反映了缓存的效果。

此外,对内存的写入可以在程序中更早地移动;在这种情况下,其他线程在程序中实际发生写操作之前可能会看到写操作。

所有这些灵活性都是通过设计来实现的——通过给编译器、运行时或硬件以最佳的顺序执行操作的灵活性,在内存模型的范围内,我们可以实现更高的性能。

实例

这方面的一个简单例子可以在以下代码中看到:

Class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}

假设这段代码同时在两个线程中执行,y的读取将看到值2。因为这个写入是在对x的写入之后进行的,所以程序员可能会假设x的读取必须看到值1。

但是,写入可能被重新排序。如果发生这种情况,那么对y的写就会发生,两个变量的读也会随之发生,然后对x的写也会发生。结果是r1的值是2,而r2的值是0。

Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存交互。它描述了程序中的变量与在真实计算机系统中存储和从内存或寄存器中存取变量的底层细节之间的关系。它以一种可以正确使用各种硬件和各种编译器优化的方式实现。

Java包含几个语言结构,包括 volatilefinalsynchronized,它们旨在帮助程序员向编译器描述程序的并发需求。

Java内存模型定义了 volatilesynchronized 的行为,更重要的是确保在所有处理器体系结构上正确地运行同步Java程序。

C++ 有 Memory Model 吗?

大多数其他编程语言,如 C 和 C++,都没有直接支持多线程。

这些语言针对编译器和体系结构中发生的各种重排序提供的保护在很大程度上依赖于所使用的线程库(如pthreads)、所使用的编译器和运行代码的平台提供的保证。

JSR 133

自1997年以来,在Java语言规范第17章中定义的Java内存模型中发现了几个严重的缺陷。

这些缺陷允许混淆行为(例如被观察到的最终字段来更改它们的值),并且破坏了编译器执行常见优化的能力。

Java内存模型是一项雄心勃勃的工作;这是编程语言规范第一次尝试合并内存模型,该模型可以为跨各种体系结构的并发性提供一致的语义。

不幸的是,定义一个既一致又直观的记忆模型比预期的要困难得多。

JSR 133为Java语言定义了一个新的内存模型,它修复了早期内存模型的缺陷。

为了做到这一点,需要更改 final 和 `volatile 的语义。

完整的语义可以在 http://www.cs.umd.edu/users/pugh/java/memoryModel 上找到,但是形式语义并不适合胆小的人。

发现像同步这样看似简单的概念到底有多复杂是令人惊讶和发人深省的。

幸运的是,您不需要了解形式语义的细节——JSR 133的目标是创建一组形式语义,为变化无常、同步和最终工作提供直观的框架。

目标

JSR 133的目标包括:

  • 保护现有的安全保证,比如类型安全,加强其他的。

例如,变量值可能不会“凭空”创建: 某些线程观察到的变量的每个值必须是一个值,该值可以被某些线程合理地放置在那里。

  • 正确同步程序的语义应该尽可能简单和直观。

  • 应该定义不完全或不正确同步的程序的语义,从而最小化潜在的安全风险。

  • 程序员应该能够自信地推断多线程程序如何与内存交互。

  • 应该可以跨广泛的流行硬件体系结构设计正确、高性能的JVM实现。

  • 应该提供一个新的初始化安全性保证。如果一个对象被正确构造(这意味着在构造过程中对它的引用不会转义),那么所有看到对该对象的引用的线程也会看到在构造函数中设置的最终字段的值,而不需要同步。

  • 对现有代码的影响应该是最小的。

旧 JMM 的缺陷

旧的内存模型有几个严重的问题。这很难理解,因此遭到了广泛的违反。

例如,在许多情况下,旧模型不允许在每个JVM中进行排序。这种对旧模型的含意的混淆促使了JSR-133的形成。

例如,人们普遍认为,如果使用了最终字段,则无需在线程之间进行同步,以确保另一个线程看到字段的值。

虽然这是一个合理的假设和明智的行为,而且实际上我们希望事情如何运作,在旧的记忆模式下,这是完全不正确的。

旧内存模型中对最终字段的处理与其他字段没有什么不同——这意味着同步是确保所有线程看到由构造函数编写的最终字段的值的惟一方法。

因此,线程可以看到字段的默认值,然后在以后的某个时候看到它构造的值。

例如,这意味着,像String这样的不可变对象似乎可以改变它们的值——这确实是一个令人不安的前景。

旧的内存模型允许使用非易失性读写对易失性写进行重新排序,这与大多数开发人员对易失性的直觉不一致,因此造成了混乱。

最后,正如我们将看到的,程序员关于他们的程序被错误同步时会发生什么的直觉常常是错误的。

JSR-133的目标之一是提请人们注意这一事实。

参考资料

https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html

http://www.cs.umd.edu/~pugh/java/memoryModel/

https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4

https://www.ibm.com/developerworks/java/library/j-jtp03304/index.html

https://www.cnblogs.com/skywang12345/p/3447546.html