Java内存模型(JSR - 133)都解决了哪些问题?

 

c01cd6cd162d01e13431a3913169ac18.jpeg

究竟什么是内存模型?

在多处理系统中,每一个 CPU 一般都包含一层或者多层内存缓存,这样设计的缘由是为了加快数据访问速度(由于数据会更靠近处理器) 而且可以减小共享内存总线上的流量(由于能够知足许多内存操做)来提升性能。内存缓存可以极大的提升性能。程序员

可是同时,这种设计方式也带来了许多挑战。面试

好比,当两个 CPU 同时对同一内存位置进行操做时会发生什么?在什么状况下这两个 CPU 会看到同一个内存值?编程

如今,内存模型登场了!!!在处理器层面,内存模型明肯定义了其余处理器的写入是如何对当前处理器保持可见的,以及当前处理器写入内存的值是如何使其余处理器可见的,这种特性被称为可见性,这是官方定义的一种说法。数组

然而,可见性也分为强可见性弱可见性,强可见性说的是任何 CPU 都可以看到指定内存位置具备相同的值;弱可见性说的是须要一种被称为内存屏障的特殊指令来刷新缓存或者使本地处理器缓存无效,才能看到其余 CPU 对指定内存位置写入的值,写入后的值就是内存值。这些特殊的内存屏障是被封装以后的,咱们不研究源码的话是不知道内存屏障这个概念的。缓存

内存模型还规定了另一种特性,这种特性可以使编译器对代码进行从新排序(其实从新排序不仅是编译器所具备的特性),这种特性被称为有序性。若是两行代码彼此没有相关性,那么编译器是可以改变这两行代码的编译顺序的,只要代码不会改变程序的语义,那么编译器就会这样作。安全

咱们上面刚提到了,从新排序不仅是编译器所特有的功能,编译器的这种重排序只是一种静态重排序,其实在运行时或者硬件执行指令的过程当中也会发生重排序,重排序是一种提升程序运行效率的一种方式。多线程

好比下面这段代码架构

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

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

当两个线程并行执行上面这段代码时,可能会发生重排序现象,由于 x 、 y 是两个互不相关的变量,因此当线程一执行到 writer 中时,发生重排序,y = 2 先被编译,而后线程切换,执行 r1 的写入,紧接着执行 r2 的写入,注意此时 x 的值是 0 ,由于 x = 1 没有编译。这时候线程切换到 writer ,编译 x = 1,因此最后的值为 r1 = 2,r2 = 0,这就是重排序可能致使的后果。并发

因此 Java 内存模型为咱们带来了什么?app

Java 内存模型描述了多线程中哪些行为是合法的,以及线程之间是如何经过内存进行交互的。Java 内存模型提供了两种特性,即变量之间的可见性和有序性,这些特性是须要咱们在平常开发中所注意到的点。Java 中也提供了一些关键字好比 volatile、final 和 synchronized 来帮助咱们应对 Java 内存模型带来的问题,同时 Java 内存模型也定义了 volatile 和 synchronized 的行为。

其余语言,好比 C++ 会有内存模型吗?

其余语言好比 C 和 C++ 在设计时并未直接支持多线程,这些语言针对编译器和硬件发生的重排序是依靠线程库(好比 pthread )、所使用的编译器以及运行代码的平台提供的保证。

JSR - 133 是关于啥的?

在 1997 年,在此时 Java 版本中的内存模型中发现了几个严重的缺陷,这个缺陷常常会出现诡异的问题,好比字段的值常常会发生改变,而且很是容易削弱编译器的优化能力。

因此,Java 提出了一项雄心勃勃的畅想:合并内存模型,这是编程语言规范第一次尝试合并一个内存模型,这个模型可以为跨各类架构的并发性提供一致的语义,可是实际操做起来要比畅想困难不少。

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

因此,咱们说的 JSR - 133 是关于内存模型的一种规范和定义。

JSR - 133 的设计目标主要包括:

  • 保留 Java 现有的安全性保证,好比类型安全,并增强其余安全性保证,好比线程观察到的每一个变量的值都必须是某个线程对变量进行修改以后的。
  • 程序的同步语义应该尽量简单和直观。
  • 将多线程如何交互的细节交给程序员进行处理。
  • 在普遍、流行的硬件架构上设计正确、高性能的 JVM 实现。
  • 应提供初始化安全的保证,若是一个对象被正确构造后,那么全部看到对象构造的线程都可以看到构造函数中设置其最终字段的值,而不用进行任何的同步操做。
  • 对现有的代码影响要尽量的小。
重排序是什么?

在不少状况下,访问程序变量,好比对象实例字段、类静态字段和数组元素的执行顺序与程序员编写的程序指定的执行顺序不一样。编译器能够以优化的名义任意调整指令的执行顺序。在这种状况下,数据能够按照不一样于程序指定的顺序在寄存器、处理器缓存和内存之间移动。

有许多潜在的从新排序来源,例如编译器、JIT(即时编译)和缓存。

重排序是硬件、编译器一块儿制造出来的一种错觉,在单线程程序中不会发生重排序的现象,重排序每每发生在未正确同步的多线程程序中。

旧的内存模型有什么错误?

新内存模型的提出是为了弥补旧内存模型的不足,因此旧内存模型有哪些不足,我相信读者也能大体猜到了。

首先,旧的内存模型不容许发生重排序。再一点,旧的内存模型没有保证 final 的真正 不可变性,这是一个很是使人大跌眼睛的结论,旧的内存模型没有把 final 和其余不用 final 修饰的字段区别对待,这也就意味着,String 并不是是真正不可变,这确实是一个很是严重的问题。

其次,旧的内存模型容许 volatile 写入与非 volatile 读取和写入从新排序,这与大多数开发人员对 volatile 的直觉不一致,所以引发了混乱。

什么是不正确同步?

当咱们讨论不正确同步的时候,咱们指的是任何代码

  • 一个线程对一个变量执行写操做,
  • 另外一个线程读取了相同的变量,
  • 而且读写之间并无正确的同步

当违反这些规则时,咱们说在这个变量上发生了数据竞争现象。 具备数据竞争现象的程序是不正确同步的程序。

同步(synchronization)都作了哪些事情?

同步有几个方面,最容易理解的是互斥,也就是说一次只有一个线程能够持有一个监视器(monitor),因此在 monitor 上的同步意味着一旦一个线程进入一个受 monitor 保护的同步代码块,其余线程就不能进入受该 monitor 保护的块直到第一个线程退出同步代码块。

可是同步不只仅只有互斥,它还有可见,同步可以确保线程在进入同步代码块以前和同步代码块执行期间,线程写入内存的值对在同一 monitor 上同步的其余线程可见。

在进入同步块以前,会获取 monitor ,它具备使本地处理器缓存失效的效果,以便变量将从主内存中从新读取。 在退出一个同步代码块后,会释放 monitor ,它具备将缓存刷新到主存的功能,以便其余线程能够看到该线程所写入的值

新的内存模型语义在内存操做上面制定了一些特定的顺序,这些内存操做包含(read、write、lock、unlock)和一些线程操做(start 、join),这些特定的顺序保证了第一个动做在执行以前对第二个动做可见,这就是 happens-before 原则,这些特定的顺序有

  • 线程中的每一个操做都 happens - before 按照程序定义的线程操做以前。
  • Monitor 中的每一个 unlock 操做都 happens-before 相同 monitor 的后续 lock 操做以前。
  • 对 volatile 字段的写入都 happens-before 在每次后续读取同一 volatile 变量以前。
  • 对线程的 start() 调用都 happens-before 在已启动线程的任何操做以前。
  • 线程中的全部操做都 happens-before 在任何其余线程从该线程上的 join() 成功返回以前。

须要注意很是重要的一点:两个线程在同一个 monitor 之间的同步很是重要。并非线程 A 在对象 X 上同步时可见的全部内容在对象 Y 上同步后对线程 B 可见。释放和获取必须进行匹配(即,在同一个 monitor 上执行)才能有正确的内存语义,不然就会发生数据竞争现象。

 

final 在新的 JMM 下是如何工做的?

经过上面的讲述,你如今已经知道,final 在旧的 JMM 下是没法正常工做的,在旧的 JMM 下,final 的语义就和普通的字段同样,没什么其余区别,可是在新的 JMM 下,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;
    }
  }
}

执行读取器的线程必定会看到 f.x 的值 3,由于它是 final 的。 不能保证看到 y 的值 4,由于它不是 final 的。 若是 FinalFieldExample 的构造函数以下所示:

public FinalFieldExample() { 
  x = 3;
  y = 4;
  // 错误的构造,可能会发生逃逸
  global.obj = this;
}

这样就不会保证读取 x 的值必定是 3 了。

这也就说是,若是在一个线程构造了一个不可变对象(即一个只包含 final 字段的对象)以后,你想要确保它被全部其余线程正确地看到,一般仍然须要正确的使用同步。

 

新的内存模型修复了双重检查锁的问题吗?

也许咱们你们都见过多线程单例模式双重检查锁的写法,这是一种支持延迟初始化同时避免同步开销的技巧。

class DoubleCheckSync{
 	private static DoubleCheckSync instance = null;
  public DoubleCheckSync getInstance() {
    if (instance == null) {
      synchronized (this) {
        if (instance == null)
          instance = new DoubleCheckSync();
      }
    }
    return instance;
  } 
}

这样的代码看起来在程序定义的顺序上看起来很聪明,可是这段代码却有一个致命的问题:它不起做用

??????

双重检查锁不起做用?

是的!

为毛?

缘由就是初始化实例的写入和对实例字段的写入能够由编译器或缓存从新排序,看起来咱们可能读取了初始化了 instance 对象,但其实你可能只是读取了一个未初始化的 instance 对象。

有不少小伙伴认为使用 volatile 可以解决这个问题,可是在 1.5 以前的 JVM 中,volatile 不能保证。在新的内存模型下,使用 volatile 会修复双重检查锁定的问题,由于这样在构造线程初始化 DoubleCheckSync 和返回其值之间将存在 happens-before 关系读取它的线程。

最后

最近我整理了整套《JAVA核心知识点总结》,说实话 ,做为一名Java程序员,不论你需不须要面试都应该好好看下这份资料。拿到手老是不亏的~个人很多粉丝也所以拿到腾讯字节快手等公司的Offer

Java进阶之路群,找管理员获取哦-!

3318f3a472606f9908bb30a47c35ed85.png

5a192ddc84eca6a291d43ec1b2b63dfe.png

相关文章
相关标签/搜索