Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能到一致的内存访问效果。
Java内存模型是个很复杂的规范,从我们程序员的角度来说主要掌握并发过程中,如何确保内存的正确性。内存模型对并发的影响主要是:
不管是增加缓存还是jvm进行编译优化,无非都是为了提升性能。所以我们更应合理地使用这些东西,在满足业务需要的同时,保证性能的最大化。Java内存模型为我们提供了volatile、锁和final三个实现,以及Happens-Before规则来帮助我们实现这种最大化。
关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义为volatile后,它将具备两种特性:
如图,普通的变量,箭头内的操作可以认为是原子操作,而箭头之间,可以产生并发,而被volatile修饰的变量,2个红色框分别可认为是原子操作,但是终究不是整体的原子操作,假设一个变量V同时被2个线程拉入了执行引擎,并改变了值,然后再执行写入,这时候主内存最终的值就不一定了。
/**
* GLC单例
*/
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在这里,关键字volatile发挥的就是它防止指令重排的作用。新建一个对象的new操作并不是一个原子性的操作,它大致可分为:
如果2和3发生指令重排,那么这个时候如果存在另外一个线程进入这个方法,判断第一道null值检查为false,直接就会返回一个没有新建完全的对象,导致错误;这个例子也说明了另外一个问题,synchronized对代码块的加锁,保证了只有一个线程进入临界区域,所以符合下文要说的happens-before规则的第一条,也进而说明锁操作,是不会禁止指令的重排序优化的。
final关键字正如我们所知道的,被final修饰的变量,一旦被初始化,则不可被修改。则它一旦被初始化(逃逸除外),就天然地对其他线程可见;
同volatile,其实锁的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则实现的。
happens-before原则
翻译为”先行发生原则“,这里的先行发生,并不是指代时间上的先后顺序,而是用来描述两个操作的内存可见性的。如果操作X happens-before操作Y,那么X的结果对于Y可见。Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。