JSR 133 FAQ

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

CPU缓存

在多处理器系统中,处理器通常会有多级高速缓存,以此减少共享内存总线上的流量。加速数据访问提高性能,但也带来了许多新的挑战。

当两个处理器同时查看同一个的内存地址时会发生什么,在什么条件下会看到相同的值?

在处理器级别,JMM定义了很多限制,用于保证当前处理器,能够看到其他处理器对内存的写入,并让当前处理器的写入,对其他处理器可见。

有些处理器表现出的内存模型比较强大,暂时称为强内存模型,能够让所有处理器看到的值始终相同。

有些处理器表现出的内存模型比较弱,暂时称为弱内存模型,需要通过特殊指令(内存屏障)来刷新。或使处理器的高速缓存失效,以实现处理器间内存的相互可见。

这些内存屏障,通常会在锁定和解锁操作时执行, 对于语言使用者来说,内存屏障是不可见的。由于减少了对内存屏障的依赖,强内存模型编写程序时会更容易。

编译优化

即使在一些最强大的内存模型上,通常也需要内存屏障。因为,代码实际运行时的位置,很多时候会和直觉不一样。处理器设计趋势,鼓励了弱内存模型,因为,这种模型放宽了缓存一致性,增加跨多个处理器和大内存时的可扩展性。

编译器对代码的重新排序,加重了写入操作对另一个线程可见性的问题。

例如,编译器有时会认为,调整某些写操作的顺序,可能会更高效,只要程序的语义(预期逻辑)没发生变化,就可以自由地进行代码顺序的调整。

如果,编译器因调整代码顺序,导致某个操作推迟,则另一个线程在执行时,不会看到推迟后的操作。此外,也可能将程序对内存的写入,调整到更早。

在这种情况下,其他线程可能会在程序“实际发生”之前看到写入。

所有这些灵活性都是经过设计的:在内存模型允许的范围内,给编译器、运行时、硬件,提供最佳执行顺序的自由,以实现更佳性能。

示例:

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内存模型描述了多线程中,代码哪些行为是合法的,以及线程如何通过内存进行交互。它描述了在真实的计算机系统中,一个程序中的变量,与内存存取细节之间的关系。它的实现方式可以通过使用各种各样的硬件,和各种各样的编译器优化来实现。

Java同步方案

Java提供的关键volatile、final、synchronized,旨在帮助开发人员,向编译器描述程序的并发需求。

Java内存模型定义了volatile和synchronized的行为,确保一个正确同步的java程序,能在所有处理器架构上正确运行。

其他语言内存模型

其他语言,如C ++有内存模型吗?

大多数编程语言,如C、C ++,并未直接支持多线程。这些语言针对编译器,和体系结构中发生的各种重排序所提供的保护,很大程度上依赖于所使用的线程库(如pthread)、编译器及运行的平台。

JSR133

自1997年以来,Java语言规范中定义的java内存模型,发现了几个严重的缺陷。这些缺陷允许混淆行为,破坏了编译器执行常见优化的能力。

Java内存模型是一项充满野心的工程,是程序语言规范,首次尝试合并一个内存模型,该内存模型可为各种体系结构的并发,提供一致的语义。

不幸的是,定义一个一致且直观的内存模型,比预期困难得多。

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

为了达到这一点,需改变final和volatile的语义。

完整语义可在The Java Memory Model上找到,但正式的语义却令人望而生畏,令人惊讶和发人深省的是,像同步这样看似简单的概念,实际上是那么的复杂。幸运的是,开发者不需要了解正式语义的细节。

JSR 133目标

JSR 133目标是通过一组语义,提供一种直观的框架,方便了解synchronized、volatile、final是如何工作的。

1.在保持现有安全基础上(如类型安全),在其他方面提供更强的安全保障。如,变量值可能不会“凭空”创建:一个线程观察到的每个变量值,必须是某个线程合理设置的值。

2.正确同步程序的语义应尽可能简单直观。应限制不完全,或不正确的同步语义,以便最大程度地减少潜在的安全隐患。

3.开发者应该能够自信地说出,多线程程序如何与内存交互。

4.可以在主流硬件架构中设计正确的、高性能的JVM实现。

5.提供初始化安全性的新保证,如果正确构造了一个对象(这表示对象的引用在构造期间不会被转义),那么,看到该对象的线程,也能看到构造方法对final属性设置的值,而不需要做任何同步。

指令重排序

很多时候,访问程序变量(属性,静态字段和数组元素)的顺序,可能与程序指定的顺序不同,编译器能够以优化的名义,自由地对指令进行排序。

处理器在某些时候,会以不同于程序指定的顺序,在寄存器、处理器高速缓存、内存之间传递数据。

如果一个线程向属性a和属性b执行写入操作,且属性b的值不依赖于属性a,那么,编译器就可以重新安排这些操作。缓存可以在“a”之前,将“b”刷新到主内存。

有许多重新排序的潜在源,如:编译器、JIT和缓存。编译器、运行时和硬件,应协同创建“as-if-serial”语义的假象。

这表示在单线程中,程序不应觉察到重新排序的影响。但重排序可能会在错误同步的多线程应用中产生作用,其中一个线程能够察觉到其他线程的产生影响。

大多数情况下,一个线程并不关心另一个线程在做什么,当发生指令重排序时,同步的意义就体现出来了。

同步作用

同步的作用有几个方面,最常理解的是互斥。

同一时刻只有一个线程,可以持有同一个监视器。因此,在监视器上执行同步,意味着一旦一个线程进入受监视器保护的同步块,其他任何线程,都无法进入受该监视器保护的块,直到第一个线程退出同步块。

但是,除了互斥之外,同步还可做的更多,同步确保同步块之前,或同步期间的线程,对内存的写入,以可预测的方式进行,并对在同一监视器上同步的其他线程可见。

在一个线程退出synchronized块之后,会释放了监视器,这时,同步块的作用是将缓存刷新到主内存中,因此,该线程所做的写操作对其他线程是可见的。

在一个线程进入同步块之前,会获取监视器,此时,同步块的作用是使处理器缓存失效,以便从主内存重新加载变量,然后,新进入的线程将能够看到,上一个线程退出同步块前所作的写操作。

从高速缓存的角度看这个问题,听起来好像这些问题只在多处理器上受影响。但是,在单个处理器上也能看到重新排序的效果。如,编译器无法将同步块中的代码,移动到获取同步之前,或释放同步之后。

当我们说获取和释放作用于缓存时,是把一些可能的效果进行了简化。

新的内存模型语义在内存操作(读取字段、写入字段、锁定、解锁),和其他线程操作上是部分有序的,当一个动作发生在另一个动作之前时,第一个动作保证排在第二个动作之前,并且可见。排序规则如下:

线程中的每个操作,都发生在该线程中后续操作之前。

监视器上的解锁,发生在同一监视器上的后续锁定之前。

对volatile属性的写入会发生在,后续每个对同一个属性读取之前。

线程上的start()调用发生在,基于该线程上的任何操作之前。

线程中的所有操作都发生在,其他线程从该线程的join()成功返回之前。

这表示在退出同步块之前,对线程可见的任何内存操作,对同一监视器上的后续线程都是可见的,因为,所有内存操作,都在释放之前发生,并且释放发生在获得之前。

另一个含义是,一些用来强制进行内存屏障的模式是不起作用,如下所示:

synchronized (new Object()) {}

这实际上是一个无效操作,并且,编译器可以完全删除它,因为编译器知道,没有任何线程将在同一监视器上同步。必须为一个线程设置一个happens-before关系,以查看另一个线程的结果。

注:两个线程在同一监视器上同步,以便正确设置happens-before关系。

Final关键字

Final字段如何在JMM规范下工作? 对象的final字段的值,会在构造方法中进行设值。

假设对象的构造方法处理得当,一旦一个对象构造完成,在构造方法中对final字段的赋值,将对其他线程可见,而不用进行任何同步操作。

正确构造

意味着在构造过程中,不允许正在构造的对象发生引用“逃逸”。

换句话说,不要在另一个线程可能看到的地方,放置正在构造的对象引用,不要将它分配给静态字段,不要将其注册成其他对象的监视器。

依此类推,这些任务应在构造方法完成后进行操作,而不应在构造方法中。

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }
}

如上所示,线程执行读取器保证看到fx的值为3 ,因为它被final修饰。

public FinalFieldExample() { // bad!
  x = 3;
  y = 4;
  // 有问题的构造过程 - 它允许对象(this)发生逃逸(escape)
  global.obj = this;
}

假如FinalFieldExample构造方法如上代码所示:global.obj不保证看到x为3。

Volatile

volatile字段是在线程之间,传递状态的特殊字段。

每次读取volatile都会看到,线程对该volatile的最后一次写入。

禁止编译器和运行时,将它们分配到寄存器中。必须确保在写入之后,将它们从缓存中刷新到主内存,以便其他线程看到。

类似地,在读取volatile字段之前,必须使高速缓存失效,以读取主内存中的值,重排序对volatile变量的访问还有其他限制。

在旧的内存模型下,对volatile变量的访问不能相互重排序,但可以让非volatile变量访问重排序。

这破坏了把volatile字段,作为从一个线程到另一个线程发送信号的手段。

新的内存模型,仍然保证volatile变量不能相互重排序。不同在于,现在对它们周围的正常字段,进行重排序不再那么容易。

volatile字段写入与监视器释放具有相同的记忆效应,从volatile字段读取具有与监视器获取相同的记忆效应。

实际上,新的内存模型对其他字段访问(volatile或非volatile)volatile字段,重新排序施加了更严格的限制,所以,当线程A对volatile字段f写入时,线程A可见的任何内容,在线程B读取f时都可见。