JMM((JavaMemoryModel)即为Java内存模型,JVM(Java Virtual Machine)是Java虚拟机,两者不能混为一谈。
其流程简单来说,就是主内存中的变量先写入到工作内存中,接着交给线程去进行运算,运算后工作内存中的变量副本值发生改变,然后再重新写入到主内存中。
工作内存中的变量副本是线程独占的,不同于主内存中的共享变量,它不能被其他线程读取到值。也就是说,如果有A、B两个线程同时运行,当A线程的进行到第④步时,变量副本值发生修改i=1,但由于没有写回到主内存中,还处于线程独占的状态,B线程便不会知道i的值发生变化,如果B线程中是一个循环,当i=1时跳出循环,那么B线程永远不会跳出循环。
那如何让B线程知道A线程中的变量值发生了改变呢?这个时候就引入了一个概念,可见性——当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
仔细的同学会发现,上述列的交互操作一共有8个,而图中只有6个,剩下的lock和unlock没有出现!而这就是volatile实现的关键点。
在讲volatile的实现原理之前,要先提一下MESI缓存一致性协议,它提供一个嗅探机制(可以简单理解为监听,具体是硬件层面的实现,个人也不太了解),在缓存(工作内存)写入到主内存时,使其他线程缓存中的该变量副本失效,不得不去主内存中获取正确的值。
volatile修饰的变量,在进行到第⑤步时,会加上一个lock操作,直到第⑥步完成之后,才会unlock,而这段时间内,别的线程是不能进行⑤⑥两步操作的。同时由于MESI的存在,线程B便能从主内存中拿到正确的i值进行操作。
一句话概括:volatile — 保证读写的都是主内存的变量。
可见性已经说过了,这里着重讲一下原子性和有序性。
public class SecondSingleton {
//volatile关键字保证可见性 同时禁用指令重排(jdk1.5后生效)
private static volatile SecondSingleton singleton;
private SecondSingleton(){
}
public static SecondSingleton getSingleton() {
if (singleton == null) {
synchronized (SecondSingleton.class) {
if(singleton == null){
singleton = new SecondSingleton();
}
}
}
return singleton;
}
}
singleton = new SecondSingleton()
这句,并非是一个原子操作,他在 JVM 中这句话大概做了下面 3 件事情:
1. 给 singleton 分配内存
2. 调用 SecondSingleton的构造函数来初始化成员变量
3. 将singleton 对象指向分配的内存空间
这时,如果进行重排序,执行顺序改为了1->3->2,同时另一个线程第3步执行完而第2步未执行时在执行语句:
if(singleton == null){
singleton = new SecondSingleton();
}
那么这时的singleton已经是有内存空间的(非null),但此时它没有成员变量,所以在实例化的时候会出现报错。
如何避免这个问题? 只需要声明变量的时候加上一个volatile便可以,它能禁止处理器的指令重排序。