假设有一个线程为变量 aVariable 赋值:
aVariable = 10;
内存模型需要解决的问题是:在什么条件下,读取 aVariable 的线程将看到这个新值。通常而言,如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到另一个线程的操作结果:在编译器中生成的指令顺序,可以与源代码中的顺序不同;编译器可能会把变量保存在寄存器而不是内存中;处理器可以采取乱序或并行等方式来执行指令;缓存可能会改变将写入变量提交到主内存的次序;保存在处理器本地缓存中的值对于其他处理器是不可见的。这些因素都会使得一个线程无法看到变量的最新值,并且会导致其他线程中的内存操作似乎在乱序执行——如果没有使用正确的同步。
Java内存模型(JMM)规定了JVM必须遵循的一组最小保证,这组最小保证规定了对变量的写入操作在何时将对于其他线程可见。
JMM 通常会额外定义一些特殊的指令(称为内存栅栏),当需要共享数据的时候,JVM 通过在适当的位置上插入内存栅栏来屏蔽JMM 与底层平台内存模型之间的差异,这样就使得开发者无须关心不同处理器架构上内存模型的差异。
JMM 是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM 为程序中的所有操作定义了一个偏序关系,称之为 Happens-Before。要想保存执行操作 B 的线程看到操作 A 的结果(无论 A 和 B 是否在同一个线程中执行),那么在 A 和 B 之间必须满足 Happens-Before 关系。如果两个操作之间不存在 Happens-Before 关系,那么 JVM 可以对它们任意地进行重排序。
Happens-Before 的规则包括:
- 程序顺序规则:如果程序中操作 A 在操作 B 之前,那么线程中 A 操作将在 B 操作之前执行。
- 监视器锁规则:在监视器锁上的释放锁操作必须在同一个监视器锁上的加锁操作之前执行。
- volatile 变量规则:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。
- 线程启动规则:在线程上对 Thread.start 方法的调用必须在该线程中执行任何操作之前执行。
- 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从 Thread.join 方法中成功返回,或者在调用Thread.isAlive 时返回 false。
- 中断规则:当一个线程在另一个线程上调用 interrupt 方法时,必须在被中断线程检测到 interrupt 调用之前执行。
- 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
- 传递性:如果操作 A 在操作 B 之前执行,且操作 B 在操作 C 之前执行,那么操作A必须在操作 C 之前执行。
有时候可以将 Happens-Before 的程序顺序规则和其他某个顺序规则(通常是监视器锁规则或者 volatile 变量规则)结合起来,从而对某个未被锁保护的变量的访问操作进行排序,称之为 piggyback。