Java 内存模型

为何要有内存模型

计算机的 CPU 和内存之间一直有一个核心矛盾,就是它们之间的运算速度有好几个数量级的差距,为了平衡它们的差别,主要作了如下:数组

  • CPU 增长了高速缓存,以均衡与内存的速度差别;
  • 操做系统增长了进程、线程,以分时复用 CPU,均衡 CPUI/O 设备的差别;
  • 编译器优化指令执行次序,使得缓存能获得更加充分的利用。

虽然高速缓存很好地解决了处理器与内存的速度矛盾,可是又出现了一个新的问题。在多核处理机中,每一个处理器都有本身的高速缓存,它们共享同一主内存。当多个处理器的任务涉及到同一块主内存区域时,可能致使缓存的数据不一致的状况,这就是可见性问题,可见性是指一个线程对共享变量的修改,另一个线程可以马上看到。缓存

操做系统基于线程来进行任务调度。高级语言的一条语句每每须要多条指令完成,可是任务切换能够发生在任何一条 CPU 指令后,在多线程环境下这就可能致使数据与预期的不一致,即原子性问题。原子性是指一个或多个操做在 CPU 执行过程当中不被中断。安全

编译器的指令重排序优化一样不能保证最终的结果与预期的一致。这里的重排序会知足如下两个条件:多线程

  • as-if-serial:在单线程环境下无论怎么重排序,不能改变程序运行的结果。
  • 数据依赖性:存在数据依赖关系的不能重排序。

须要注意的是:虽然重排序不会影响单线程环境的执行结果,可是会破坏多线程的执行语义。也就是有序性问题,有序性指的是程序按照代码的前后顺序(逻辑前后)执行。并发

因此,JVM 试图虚拟机定义了一种 Java 内存模型(Java Memory ModelJMM)来屏蔽掉各层硬件和操做系统的内存访问差别,以实现让 Java 程序在各类平台下都能达到一致的内存访问效果,也就是解决以上三个问题。app

Java 内存模型

Java 内存模型主要是为了定义程序中各个变量的访问规则,此处的变量指的是实例字段、静态字段和构成数组对象的元素等共享变量。学习

Java 内存模型规定了全部的变量都存储在主内存中。每一个线程还有本身的工做内存,其中保存了该线程使用的变量的主内存副本拷贝,线程对变量的全部操做必须在工做内存中进行。不一样线程之间也没法直接访问对方工做内存中的变量,线程之间的变量值传递须要经过主内存来完成。优化

线程、工做内存、主内存三者的关系以下:this

内存间交互操做

对于主内存与工做内存之间交互的实现细节,Java 内存模型中定义了 8 种操做来实现,虚拟机实现时必须保证这些操做是原子性的。操作系统

  • read(读取):把一个变量的值从主内存传输到工做内存中;
  • load(载入):把 read 操做从主内存获得的变量放入工做内存的变量副本中;
  • use(使用);把工做内存中一个变量的值传递给执行引擎;
  • assign(使用):把一个从执行引擎接收到的值赋给工做内存的变量;
  • store(存储):把工做内存中一个变量的值传送到主内存中;
  • write(写入):把 store 操做从工做内存中获得的变量放入主内存的变量中。

volatile 型变量

关键字 volatileJVM 提供的轻量级的同步机制。当一个变量被定义为 volatile 后,它能够保证内存的可见性。

使用 volatile 还能够禁止指令重排序优化。它是 Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

内存屏障(Memory Barrier)是一组处理器指令,用于实现对内存访问操做的顺序限制。在重排序时不能把后面的指令重排序到内存屏障以前的位置。

原子性、可见性、有序性

Java 内存模型保证了并发的三个特性:原子性、可见性、有序性,下面学习一下哪些操做实现了这三个特性:

原子性

Java 内存模型保证了内存间交互的 8 个操做的原子性,但对于 64 位的数据类型(longdouble),容许虚拟机的实现能够不保证 64 位数据类型的 loadstorereadwrite4 个操做的原子性。但目前虚拟机几乎都把 64 位数据的读写操纵做为原子性来对待。也就是说能够认为基本类型的读写访问是具有原子性的。

JMM 还提供了 lockunlock 操做来保证更大范围的原子性,尽管虚拟机并未将其开放给用户,但可以使用 monitorentermonitorexit 字节码指令来隐式地使用这两个操做,对应到 Java 代码中就是 synchronized 关键字,因此 synchronized 同步块也是原子性的。

可见性

主要有三种方式实现可见性:

  • volatilevolatile 保证了新值能当即同步到主内存,以及每次使用前当即从主内存刷新。
  • synchronized:对同步块加锁解锁,在执行 unlock 操做前必须把此变量值同步到主内存中。
  • final:被 final 关键字修饰的字段在构造器中一旦初始化完成,而且没有发生 this 引用逃逸(其它线程可能经过引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

有序性

Java 中有两种方式保证线程之间操做的有序性;

  • volatile 关键字经过添加内存屏障的方式来禁止指令重排。
  • 经过 synchronized 来保证有序性,它保证每一个时刻只有一个线程执行同步代码,即让线程串行地执行同步代码。

Happends-Before 原则

前面说的保证并发安全的定义实践起来比较麻烦,有一个等效判断原则——Happens-Before 原则,来肯定一个访问在并发环境下是否安全。

Happens-Before 的含义就是前面一个操做的结果对后续操做是可见的。要想保证执行操做 B 的线程看到线程 A 的结果,那么 AB 之间必须知足 Happens-Before 原则。若是两个操做之间缺少 Happens-Before 原则,那么 JVM 就能够对它们任意地重排序,那么就会产生数据竞争问题。

Happens-Before 原则包括:

  • 程序顺序规则:一个线程内按照控制流顺序,前面的操做 Happens-Before 于后面的操做。
  • 管程锁定规则:一个 unlock 操做 Happens-Before 于后面对同一个锁的 lock 操做。
  • volatile 变量规则:对一个 volatile 变量的写操做 Happens-Before 于对该变量的读操做。
  • 线程启动规则:Thread 对象的 start 方法 Happens-Before 于此线程的每个动做。
  • 线程终止规则:线程中的全部操做都 Happens-Before 于对该线程的终止检测,可经过 Thread.join 方法结束,或 Thread.isAlive 方法的返回值,检测到线程已经终止执行。
  • 线程中断规则:对线程 interrupt 方法的调用 Happens-Before 于被中断线程的代码检测到中断事件的发生。
  • 对象终结规则:一个对象的初始化完成 Happens-Before 于它的 finalize 方法的开始。
  • 传递性:若是操做 A Happens-Before 于操做 B,操做 B Happens-Before 于操做 C,那么操做 AHappens-Before 于操做 C

参考资料

相关文章
相关标签/搜索