JSR 133 (Java Memory Model) FAQ - 上

最近学习 Java 内存模型的相关知识,找到 Java 提案内存模型(JSR 133)的相关文章

The Java Memory Model

里面提到关于常见问题解答的文章

JSR 133 (Java Memory Model) FAQ

已有中文翻译 JSR 133 (Java Memory Model) FAQ,还是想自己试一下


主要内容:

  1. 究竟什么是内存模型?(What is a memory model, anyway?
  2. 类似于 C++ 等其它语言有内存模型吗?(Do other languages, like C++, have a memory model?
  3. JSR 133 讲些什么?(What is JSR 133 about?
  4. 重排序(reordering)指什么?(What is meant by reordering?
  5. 之前的内存模型有哪些错误?(What was wrong with the old memory model?
  6. 错误同步意味着什么?(What do you mean by "incorrectly synchronized"?
  7. 同步(synchronization)做了哪些事情?(What does synchronization do?
  8. final 字段是如何改变值的?(How can final fields appear to change their values?
  9. final 字段在新的 Java 内存模型(JMM)下如何工作?(How do final fields work under the new JMM?
  10. volatile 做了什么?(What does volatile do?
  11. 新的内存模型是否解决了”双重校验锁”(double-checked locking)的问题?
  12. 我应该怎么写一个虚拟机?(What if I'm writing a VM?
  13. 我应该关心哪些事情?(Why should I care?)

引言

FAQ 概述了 Java 内存模型,重排序,同步,final 字段和 volatile 字段,以及双重校验锁。

因为内容较多,分为上下两篇


究竟什么是内存模型?

在多处理器系统中,处理器通常拥有一级或者多级缓存,缓存层不仅能加快数据访问的速度(缓存数据离处理器更近),而且能减少共享内存总线的流量(缓存数据就能满足处理器数据需求,不需要再访问内存)(reducing traffic on the shared memory bus(because many memory operations can be satified by local caches))。缓存能极大的提高性能,也由此出现了很多新的问题。比如,当两个处理器同时访问一个内存地址时会发生什么?哪种情况下处理器可以看见相同的值?(What, for example, happens when two processors examine the same memory location at the same time? Under what conditions will they see the same value?

在处理器级别,一个内存模型定义应该实现的充分必要条件necessary and sufficient conditions)是其它处理器写入内存的数据对于当前处理器是可见的,当前处理器写入内存的数据对其它处理器也是可见的。如果是一个强大的内存模型(a strong memory model),那么所有处理器在任何时候读取内存中任何位置,都能得到一样的值;而对于较弱的内存模型(a weaker memory model),只有在特殊情况下,通过调用内存栅栏(where special instructions, called memory barriers)将缓存数据刷新或者失效(flush or invalidate the local processor cache),这样才能获取其它处理器处理后的数据或者让其它处理器知道当前处理器处理的数据。这些内存栅栏(memory barriers)通常在加锁和解锁操作的时候执行;它们对于高级语言(a high level language)而言是不可见的。

有时候在强内存模型中写程序比较容易,因为减少了对内存栅栏的需要。然而,即便是在一些最强的内存模型中,内存栅栏往往也是必要的,而且它们的位置经常是违反直觉的(counterintuitive)。当前处理器设计的趋势是鼓励弱内存模型,因为弱内存模型对于缓存一致性的放宽使得多处理器和大内存有更大的可扩展性(becuase the relaxations they make for cache consistency allow for greater scalability across multiple processors and larger amounts of memory)。

当前线程的写操作何时对其它线程可见,这个问题因为编译器的重排序而变得复杂(The issue of when a write becomes visible to another thread is compounded by the compiler's reordering of code.)。比如,编译器可能会觉得往后移动一个写操作会更加有效率;只要这个代码移动不会改变程序语义,这样做没有任何问题。如果编译器推迟了一个操作,只有到程序运行完成,其它线程才能看见数据的变化,这就是缓存的作用。(if a compiler defers an operation, another thread will not see it until it is performed; this mirrors the effect of caching,这句话的意思应该是由于缓存的存在,指令重排序不会马上影响到内存中数据的变化,因为操作缓存数据即可)。

另外,写内存操作也可以移动到前面,在这种情况下,其他线程可能会提前看见数据的变化。所有这些操作都可以设计出来 - 通过赋予编译器,运行时环境,或者硬件以最佳顺序执行的灵活性。

下面代码展示了一个简单的示例:

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

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

当两个线程并发(concurrently)执行这段代码时,y 的值是 2。因为对于 y 的写入操作在 x 之后,所以程序员可能会假设 x 的值是 1。但是,读操作可能被重排序了。如果真的发生了,而且在 y 的赋值操作结束后,执行了两个变量的读操作,最后才是 x 的赋值操作。此时,r1 的值是 2,但是 r2 的值是 0

Java 内存模型描述了在多线程代码中,哪种行为是合法的,以及线程之间如何 通过内存交互The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory)。它描述了程序中变量之间的关系,以及在实际计算机的内存或者寄存器中存储和检索它们的低级细节(It describes the relationship between variables in a program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system)。它可以通过各种各样的硬件和各种各样的编译器优化来实现(It does this in a way that can be implemented correctly using a wide variety of haredware and a wide variety of compiler optimizations)。

Java 拥有几种语言构造,包括 volatilefinalsynchronized 关键字,它们旨在帮助程序员描述程序对编译器的并发要求(Java includes several language constructs, including volatile, final, and synchronzied, which are intended to help programmer describe a program's concurrency requirements to the compiler)。Java 内存模型定义了 volatilesynchronized 的行为,更重要的是,确保了一个正确的同步 Java 程序可以正确的运行在所有处理器架构上。

个人见解

Java 内存模型是为了屏蔽不同硬件处理器架构以及指令重排序对程序的并发影响而提出的一套规范,保证正确应用这套规范的程序可以在任何硬件处理器上都能够执行正确的并发操作。


类似于 C++ 等其它语言有内存模型吗?

大多数其它编程语言,比如 CC++,没有被设计为直接支持多线程。这些语言对于编译器和硬件架构中指令重排序的屏蔽严重依赖于使用的线程库(比如 pthreads),编译器以及代码运行的平台。

个人见解

Java 内存模型是 Java 虚拟机的一部分,通过屏蔽程序和运行平台的联系,真正实现 write once,run anywhere


JSR 133 讲些什么?

自从 1997 年以来,在 Java 语言规范17 章定义的 Java 内存模型被发现有几个严重的缺陷。这些缺陷允许令人困惑的行为(比如,final 字段被发现可以改变值),并且破坏了编译器执行常见优化的能力(These flaws allowed for confusing behaviors(such as final fields being observed to change their value) and undermined the compiler's ability to perform common optimizations)。

Java 内存模型是一项雄心勃勃的任务;这是第一次编程语言规范视图将一个内存模型结合起来,它可以在各种架构中提供一致的并发语义it was the first time that a programming language specification attempted to incorporate a memory model which could provide consistent semantics for concurrency across a variety of architectures)。不幸的是,定义一个一致性并且直观(consistent and intuitive)的内存模型远比想象的困难。JSR 133 定义了一个新的内存模型,修复了之前内存模型的缺陷。为实现这一目的,finalvolatile 的语义会有所变化。

完整的语义如下:The Java Memory Model。不需要了解正式语义的细节 - JSR 133 的目的就是创建一组正式语义,提供一个直观的框架,用于了解 volatilesynchronized 以及 final 是如何工作的。

JSR 133 的目标如下:

  • 保留已经存在的安全保证,比如类型安全,并加强其它部分。举个例子,变量值不能凭空出现:每一个线程变量的值都可以被其它线程合理的替换(For example, variable values may not be created "out of thin air": each value for a variable observed by some thread must be a value that can reasonably be placed there by some thread)。
  • 正确的同步程序的语义应该尽可能的简单和直观(The semantics of correctly synchronized programs shuold be as simple and intuitive as possible)。
  • 不完整或者不正确的同步程序的语义应该被重新定义,以使潜在的安全危险最小化(The semantics of incompletely or incorrectly synchronized programs should be defined so that potential security hazards are minimized.)。
  • 程序员应该能够自信的推理出多线程程序是如何与内存交互的(Programmers should be able to reason confidently about how multithreaded programs interact with memory.)。
  • 应该能够在广泛的流行硬件架构上设计出正确的,高性能的 JVM 实现(It should be possible to design correct, high performance JVM implementations across a wide range of popular hardware architectures.)。
  • 应该为初始化安全提供新的保证。如果一个对象被正确构造(这意味着引用该对象不会在构造期间 escape),那么不需要同步,所有引用该对象的线程就能够看到在构造器设置的 final 字段值(If an object is properly constructed (which means that references to it do not escape during construction), then all threads which see a reference to that object will also see the values for its final fields that were set in the constructor, without the need for synchronization.)。
  • 对于现存的代码影响最小(There should be minimal impact on existing code.)。

个人见解

本小节讲了 JSR 133 出现的原因:因为之前的 Java 内存模型发现了一些严重缺陷;以及新的内存模型的目标。

什么是 JSR

JSR,Java Sepcification RequestsJava 规范提案

什么是 JSR 133

JSR 133: Java Memory Model and Thread SpecificationJava 内存模型与线程规范

中文版:JSR133中文版


重排序(reordering)指什么?

很多情况下程序变量(对象实例域,类静态域,数组元素)的访问不按程序指定的顺序执行。编译器可以为了优化肆意地修改指令的顺序。处理器也可能在某些情况下不按顺序执行指令。数据也可能不按程序指定顺序,在寄存器,缓存和主内存之间移动。

举个例子,如果线程写入变量 a,然后写入变量 b,那么 b 的值不依赖于 a 的值,这种情况下,编译器可以自由的重排序这些操作,缓存也可以在 a 之前刷新 b 的值到主内存。有许多可能会重排序的地方,比如编译器,JITjust-in-time compiler),缓存。

编译器,运行时环境(应该指虚拟机),以及硬件设备应该让程序认为是在按顺序执行,这样在单线程环境下,程序不需要去研究重排序的影响。然而,重排序会影响不正确的并发多线程程序,因为线程之间相互影响,对于当前线程访问的变量何时对其它线程可见,可能不按程序指定的顺序执行。

大多数时候,一个线程不需要去关心另一个线程在做什么。如果需要关心,那就是同步的部分。

个人见解

重排序指的是程序在编译,加载,链接,运行的过程中,随时都有可能程序指令被重新排列,同时,这一过程也会给上层显示有一种按序执行的假象。对于单线程程序,不需要关心重排序带来的影响;对于多线程程序,如果访问同一个变量,重排序可能会影响结果的正确性,所以需要同步。


之前的内存模型有哪些错误?

在老内存模型中有一些严重的错误。这些错误很难理解,所以很多人没有发现。比如,在大多数情况下,老内存模型不允许虚拟机中实现各种方式的重排序。对于老内存模型含义的困惑促使了 JSR-133 的出现。

举个例子,一个广泛认同的概念是如果变量使用了 final 修饰符,那么不需要在多线程中使用同步操作以保证变量的值能被其它线程可见。虽然这是一个合理的假设,也就是一个明智的行为,实际上我们也希望程序是这样执行的,但是在老内存模型中,并不是如此。老内存模型没有对 final 变量执行任何操作,这意味着同步是唯一的手段来保证所有线程能看到 final 变量在构造器写入的值。在这种情况下,线程可能会先看到 final 变量的默认值,然后又看到 final 变量在构造器写入的值。这也意味着,类似于 String 这样的不可变对象会改变它的值。

老内存模型允许非 volatile 写入被重排序为非 volatile 写和读,这也不符合开发的对于 volatile 的理解,因此也会造成困惑。

最后,因为老内存模型的缺陷,程序员对于不正确同步程序的直观体验也经常是错的。JSR-133 的目标之一就是让大家关注这一现象。

个人见解

本小节提出了两个老内存模型出现的问题,一个是 final 修饰符没有起到线程安全的作用,这也会导致不可变对象的失效;第二个是对 volatile 关键字的错误使用,没有保证 volatile 修饰的变量对于所有线程的可见性。


错误同步意味着什么?

对于不同人来说不正确的同步代码意味着不同的事情。在 Java 内存模型的语境下,当我们讨论不正确的同步代码,指的是以下这些代码:

  1. 一个线程在执行一个变量的写操作;
  2. 另一个线程会执行这个变量的读操作;
  3. 写和读操作没有同步(the write and read are not ordered by synchronization

当上述情况发生时,称之为数据竞争(data race)。有数据竞争出现的程序是不正确的同步程序

个人见解

解释了错误同步的判断,就是没有同步操作,却有多线程对同一变量执行读取操作,这就是不正确的。


同步(synchronization)做了哪些事情?

同步有好几个方面。最好理解的是互斥mutual exclusion)- 每次仅有一个线程可以拥有一个监视器,所以监视器上的同步意味着一旦线程进入了被监视器保护的同步块,那么其它线程无法进入该同步块,直到第一个线程退出该同步块为止。

同步不仅仅只有互斥。同步确保了当前线程在进入同步块或者在同步块中执行的写操作以可预测的方式对于其它阻塞的线程可见Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor.)。当线程退出同步块后,释放监视器,这将会把缓存数据刷新到主内存,所以这个线程执行的写操作对于其它线程来说是可见的。在其它线程进入同步块之前,需要获取监视器,这将会把缓存数据失效,所以需要从主内存读取数据,这样能够保证线程数据是可见的。

从缓存的角度讨论,好像上面这些问题仅仅影响到多处理器机器。然而,在单处理器中也可以很容易的找到重排序的影响。举个例子,对于编译器来说,不可能获取监视器之前或者释放监视器之后移动代码。当我们称获取和释放动作作用在缓存中时,它代表着许多可能的影响(It is not possible, for example, for the compiler to move your code before an acquire or after a release. When we say that acquires and releases act on caches, we are using shorthand for a number of possible effects.)。

新的内存模型在内存操作(写操作,读操作,加锁,解锁)和其它线程操作(startjoin)中执行部分排序,就是说一些操作一定会在另一些操作之前,称之为 happens-before。当一个动作 happens-before 另一个动作,那么保证第一个动作在第二个动作之前执行,并且其结果对于第二个动作可见。这种排序规则如下:

  • 当前线程的每一个动作都 happens-before 程序中后来的线程
  • 对同一个监视器的解锁操作 happens-before 后续的加锁操作
  • 对一个 volatile 变量的读操作 happens-before 后续对同一个变量的读操作
  • 对一个线程的 start() 调用 happens-before 这个线程启动后的任何动作
  • 当前线程的所有动作都 happens-before 其它线程(对当前线程执行 join() 操作)

这意味着当一个线程退出代码块之后,另一个线程进入代码块之前,所有的内存操作对于阻塞在同一个线程的线程都是可见的,因为当前线程的所有内存操作 happens-before 当前线程将监视器释放,释放操作 happens-before 其它线程对于监视器的获取。

这也意味着下面这条语句不起作用,一些人想要用它来强力获取一个内存栅栏:

synchronized (new Object()) {}

因为没有操作,所以编译器可以任意移动它,因为它知道没有任何其它线程同步在这个监视器。如果你想要让一个线程看到另一个线程的结果,那么需要设置一个 happens-before 关系。

Note:对于同步在同一个监视器的线程来说,设置一个正确的 happens-before 关系是很重要的。并不是说,线程 A 同步到对象 X,或者线程 B 同步到对象 Y,它们就可以看见数据变化。监视器的释放和获取需要匹配(也就是说,应该在同一个监视器上执行)到正确的语义。否则,代码将会发生数据竞争(这样就是不正确的同步程序)。

个人见解

讲解了同步操作包含的事情:

  1. 互斥。每次仅有一个线程进入同步块。
  2. 保证进入同步块的线程操作对于其它阻塞在同一个监视器的线程可见。
  3. 新的 Java 内存模型实现了 happens-before 原则,保证了同步操作的正确性

Memory Barrier

Memory Barrier:保证内存访问顺序,Memory Barrier 之前的内存访问操作必定在其之后完成

参考:

理解 Memory barrier(内存屏障)

Linux内核同步机制之(三):memory barrier

猜你喜欢

转载自blog.csdn.net/u012005313/article/details/81226956