java concurrency in practice 读书笔记-java内存储器模型

java concurrency in practice 读书笔记--java内存模型

什么是内存模型?为什么需要它?

假设一个线程为变量aVariable赋值:

avariable=3;

内存模型需要解决这个问题:在什么条件下,读取aVariable的线程将看到这个值为3?

这个问题似乎听起来很荒谬,当时在多线程情况下,将会有许多因素使得线程无法立即、甚至永远看到另一个线程的操作结果。例如:在编译器中生成的指令顺序,可以与源代码中的顺序不同----重排序;编译器可能把变量保存在寄存器而不是内存中;处理器可以采用乱序或并行等方式来执行指令;缓存可能会改变变量提交到主内存的次序;保存在处理器本地缓存中的值对于其他处理器是不可见的。

为了使java开发人员无须关心不同架构上内存模型之间的差异,java还提供了自己的内存模型,并且JVM通过在适当的位置插入内存栅栏来屏蔽JMM与底层平台内存模型之间的差异。

Java内存模型(JMM)是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。


重排序

内存模型描述了程序的可能行为。具体的编译器实现可以产生任意它喜欢的代码 -- 只要所有执行这些代码产生的结果,能够和内存模型预测的结果保持一致。这为编译器实现者提供了很大的自由,包括操作的重排序。

编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。重排序后的指令,对于优化执行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在计算性能上有了很大的提升。

重排序类型包括:

  1. 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本
  2. 处理器可以乱序或者并行的执行指令。
  3. 缓存会改编写入提交到主内存的变量的次序

内存可见性

由于现代可共享内存的多处理器架构可能导致一个线程无法马上(甚至永远)看到另一个线程操作产生的结果。所以 Java 内存模型规定了 JVM 的一种最小保证:什么时候写入一个变量对其他线程可见。

在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致。假设线程 A 写入一个变量值 V,随后另一个线程 B 读取变量 V 的值,在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:

  1. 执行线程A的处理器把变量V缓存到寄存器中。
  2. 执行线程A的处理器把变量V缓存到自己的缓存中,但还没有同步刷新到主内存中去。
  3. 执行线程B的处理器的缓存中有变量V的旧值。

Happens-before 关系

happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。

下面介绍 happens-before 关系法则:

  1. 程序次序法则:如果在程序中,所有动作 A 出现在动作 B 之前,则线程中的每动作 A 都 happens-before 于该线程中的每一个动作 B。
  2. 监视器锁法则:对一个监视器的解锁 happens-before 于每个后续对同一监视器的加锁。
  3. Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
  4. 线程启动规则:在线程上对Thread.start()的调用必须在该线程中执行任何操作之前执行。
  5. 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.alive是返回false
  6. 中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行
  7. 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
  8. 传递性:如果 A happens-before 于 B,且 B happens-before C,则 A happens-before C。

参考文献:《Java concurrency in practice》第16章    IBM develperWorks文档