什么是内存模型
在多处理器系统中,处理器通常具有一层或多层存储器高速缓存,这通过加速对数据的访问(因为数据更接近处理器)和减少共享存储器总线上的流量来提高性能(因为可以满足许多存储器操作通过本地缓存)。内存缓存可以极大地提高性能,但它们带来了许多新的挑战。例如,当两个处理器同时检查相同的内存位置时会发生什么? 在什么条件下他们会看到相同的值?
在处理器级别,存储器模型定义了必要且充分的条件,用于知道当前处理器对其他处理器的存储器写入是可见的,并且当前处理器的写入对于其他处理器是可见的。某些处理器表现出强大的内存模型,其中所有处理器始终看到任何给定内存位置的完全相同的值。其他处理器表现出较弱的内存模型,其中需要特殊指令(称为内存屏障)来刷新或使本地处理器高速缓存无效,以便查看其他处理器进行的写入或使该处理器的写入对其他处理器可见。这些内存屏障通常在执行锁定和解锁操作时执行; 对于高级语言的程序员来说,它们是不可见的。
由于减少了对内存屏障的需求,有时可以更容易为强内存模型编写程序。然而,即使在一些最强大的内存模型上,通常也需要内存屏障; 很多时候他们的位置是违反直觉的。处理器设计的最新趋势鼓励了较弱的内存模型,因为它们对高速缓存一致性的放松允许跨多个处理器和较大内存量的更大可伸缩性。
当一个写对另一个线程可见时,编译器重新排序代码的问题就变得复杂了。例如,编译器可能会决定稍后在程序中移动写操作更有效; 只要此代码动作不会改变程序的语义,就可以自由地执行此操作。如果编译器推迟操作,则另一个线程在执行之前不会看到它; 这反映了缓存的效果。此外,可以在程序中更早地移动对存储器的写入; 在这种情况下,其他线程可能会在程序实际“发生”之前看到写入。所有这些灵活性都是通过设计 - 通过为编译器,运行时或硬件提供以最佳顺序执行操作的灵活性,在内存模型的范围内,我们可以实现更高的性能。
以下代码中可以看到一个简单的示例:
1 | class Reordering { |
假设这个代码同时在两个线程中执行,并且y的读取看到值2。因为这个写入在写入x之后,程序员可能会认为读取x必须看到值1。但是,写入可能已经重新排序。如果发生这种情况,则可能会发生对y的写入,可能会读取两个变量,然后可能会发生对x的写入。结果是r1的值为2,但r2的值为0。
Java内存模型描述了多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了程序中变量之间的关系,以及在真实计算机系统中存储和检索它们与存储器或寄存器之间的低级细节。它以一种可以使用各种硬件和各种编译器优化正确实现的方式实现。
Java包括几种语言结构,包括volatile,final和synchronized,它们旨在帮助程序员描述程序对编译器的并发性要求。Java内存模型定义了volatile和synchronized的行为,更重要的是,确保正确同步的Java程序在所有处理器体系结构上正确运行。
其他语言,比如C ++,有内存模型吗?
大多数其他编程语言(如C和C ++)的设计并未直接支持多线程。这些语言针对编译器和体系结构中发生的各种重新排序提供的保护在很大程度上取决于所使用的线程库(例如pthread),所使用的编译器以及运行代码的平台所提供的保证。
什么是JSR 133?
自1997年以来,在Java语言规范的第17章中定义的Java内存模型中发现了几个严重的缺陷。这些缺陷允许混淆行为(例如观察到final字段改变其值)并破坏编译器执行常见优化的能力。
Java内存模型是一项雄心勃勃的事业; 这是编程语言规范第一次尝试合并一个内存模型,该内存模型可以为各种体系结构的并发提供一致的语义。不幸的是,定义一致且直观的内存模型证明比预期困难得多。不幸的是,定义一致且直观的内存模型证明比预期困难得多。JSR 133为Java语言定义了一个新的内存模型,它修复了早期内存模型的缺陷。为了做到这一点,需要改变final和volatile的语义。
完整的语义可以在http://www.cs.umd.edu/users/pugh/java/memoryModel上找到,但是正式的语义并不适合胆小的人。令人惊讶和发人深省的是,发现像同步一样复杂的看似简单的概念。幸运的是,您无需了解形式语义的细节 - JSR 133的目标是创建一组形式语义,为volatile,synchronized和final工作提供直观的框架。
JSR 133的目标包括:
- 保持现有的安全保障,如类型安全,并加强其他安全保障。例如,变量值可能不会“凭空”创建:某个线程观察到的变量的每个值必须是某个线程可以合理放置的值。
- 正确同步程序的语义应尽可能简单直观。
- 应定义不完全或不正确同步程序的语义,以便最大限度地减少潜在的安全隐患。
- 程序员应该能够自信地说明多线程程序如何与内存交互。
- 应该可以在广泛的流行硬件架构中设计正确,高性能的JVM实现。
- 应提供初始化安全性的新保证。如果正确构造了一个对象(这意味着对它的引用在构造期间不会被转义),那么看到对该对象的引用的所有线程也将看到在构造函数中设置的最终字段的值,而不需要同步。
- 对现有代码的影响应该是最小的。
重排序是什么意思?
在许多情况下,对程序变量(对象实例字段,类静态字段和数组元素)的访问可能看起来以与程序指定的顺序不同的顺序执行。编译器可以自由地使用优化名称中的指令顺序。处理器可能在某些情况下不按顺序执行指令。可以以不同于程序指定的顺序在寄存器,处理器高速缓存和主存储器之间移动数据。
例如,如果一个线程写入字段a然后写入字段b,并且b的值不依赖于a的值,则编译器可以自由地重新排序这些操作,并且缓存可以自由地将b在a之前刷新到主内存。有许多潜在的重新排序源,例如编译器,JIT和缓存。
编译器,运行时和硬件应该合谋创建as-if-serial语义的假象,这意味着在单线程程序中,程序不应该能够观察重新排序的影响。然而,重新排序可以在不同步的多线程程序中发挥作用,其中一个线程能够观察其他线程的影响,并且可以检测到变量的访问以不同的顺序对其他线程可见,而不是在程序中执行或指定的。
大多数时候,一个线程不关心另一个线程在做什么。但如果是这样,那就是同步的原因。