前面的章节中,我们尽可能的避开了Java内存模型(JMM)的底层细节,而将重点放在一些高层设计问题,例如:安全发布、同步策略的规范以及一致性等。其实,它们的安全性都来自于JMM。
1. 什么是内存模型,为什么需要它value = 3;
上面的变量value的赋值在什么时候可以被其他线程所看到?这听起来似乎是一个愚蠢的问题,但如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远地看到这个赋值地结果。
导致其他线程无法及时看到最新值地一些因素:
- 编译器中生成的指令顺序,可以与源码中地顺序不同
- 编译器还会把变量保存在寄存器中而不是内存中
- 处理器可以乱序或并行等方式来执行指令
- 缓存可能会对变量的写入操作提交到主存地次序产生影响
- 保存在处理器本地缓存的值,对其他地处理器是不可见的
在多线程环境中,维护程序地串行性将导致很大地开销。但当多个线程共享数据时,我们不得不通过同步操作告诉JVM在何时发生线程间地协调操作。而JMM规定了JVM必须遵守地一组最小保证,这组保证规定了对变量的写入操作将会何时对于其他线程可见。
1.1 平台的内存模型在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并且定期地与主存进行协调(其实就是同步/更新变量的值)。
不同的处理器架构中提供了不同级别的缓存一致性,其中一部分只提供最小的保证(最差的一致性保证),即允许不同的处理器在任意时刻从同一个存储位置上看到不同的值。操作系统、编译器以及运行时 需要弥补这种在硬件能力与线程安全需求之间的差异。
要想确保每个处理器都能在任意时刻知道其他处理器正在做什么,这种级别的一致性保证将需要非常大的开销,大多数时间里这么做是不必要的,因此处理器会适当放宽处理器的一致性保证——由具体的处理器架构定义的内存模型来告诉应用程序可以从内存中获得怎么的一致性保证,并提供一些特殊的指令(称为 内存栅栏,用于与主存同步共享数据)
为了使Java开发人员无需关心不同处理器架构上的内存模型的差异,Java提供了自己的内存模型,并且JVM通过在适当位置插入内存栅栏来屏蔽在JMM与底层平台的内存模型上的差异。
1.2 重排序没有充分同步的程序中,除了线程之间交替执行的顺序无法预知,JMM也会使得不同线程看到的操作执行顺序不同,这使得在缺乏同步的情况下要推断操作的执行顺序变得更加复杂。
同步将限制编译器、运行时和硬件对内存操作重排序的方式,从而在实施重排序时不会破坏JMM提供的可见性保证(在大多数主流的处理器架构中,内存模型都非常强大,使得读取volatile变量的性能与读取非volatile变量的性能大致相当)
1.3 Java内存模型简介Java内存模型时通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。
JMM为程序中所有的操作都定义了一个偏序关系(你可以理解成一个程序执行时候的一些既定的习惯),称之为Happens-before,满足此关系的操作之间将表现出串行一致性。要想满足执行操作B的线程可以看到操作A的结果,那么在A和B之间必须满足Happens-before关系,否则JVM可以对它们任意的重排序。
人话:满足 正确同步&Happens-before 的代码不会被JMM重排序
Happens-before的规则包括:
程序顺序规则:如果程序中操作A在操作B之前,那么A将先于B而执行 监视器锁规则:在监视器上面的解锁操作必须在同一监视器锁上的加锁操作之前执行(内置锁、显式锁都有这种内存语义) volatile变量规则:对于volatile变量的写入操作必须在其读取操作之前执行(原子变量也是如此) 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或从Thread.join中返回,或者在调用Thread.isAlive时返回false 中断规则:当线程A在线程B上调用interrupt时,线程A必须在被中断线程(B)检测到interrupt调用之前执行(执行啥?通过抛出InterruptException,或调用isInterrupted和interrupted) 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成 传递性:操作A先于B,B先于C -> 操作A先于操作C



