一、什么是JMM
Java线程内存模型跟CPU缓存模型类型,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
二、JMM数据原子操作
- read(读取):从主内存读取数据;
- load(载入):将主内存读取到的数据写入工作内存;
- use(使用):从工作内存读取数据来计算;
- assign(赋值):将计算好的值重新赋值到工作内存中;
- store(存储):将工作内存数据写入主内存;
- write(写入):将store过去的变量值赋值给主内存中的变量;
- lock(锁定):将主内存变量加锁,标识为线程独占状态;
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
三、JMM缓存不一致问题
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他CPU没法去读或写这个数据,直到这个CPU使用完数据释放锁之后其他cpu才能读取该数据。
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
结合上图,我们接着来进一步理解缓存一致性协议,volatile的实现跟它有关,所以要好好理解这个协议。以下是volatile底层MESI缓存一致性协议保证共享变量父本在内存可见性的大致流程。
(1)假设线程1的那个CPU开启了缓存一致性协议,那么线程1的CPU会对总线进行监听。
(2)当线程2这个CPU对initflag这个变量的值进行了修改,在store回主内存的过程中会经过总线,此时监听到initflag被修改后,线程1会将自己工作内存中的initflag置为失效。
(3)initflag修改后的值被write到主内存中。
(4)线程1发现自己工作内存中的值被置为无效后,会重新从主内存中read值出来并且load回工作内存。
注:volatile的实现是C语言,所以在Idea工具里面我们按住Ctrl键+鼠标点击是进不去看源码的。通过查看Java代码生成的汇编代码,我们可以知道,用volatile修饰的Java代码,在汇编代码中会有对应的lock指令,通过该指令就会开启MESI缓存一致性协议和cpu总线的嗅探机制。
四、Volatile可见性底层实现原理
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并写回到主内存。
IA-32架构软件开发者手册对lock指令的解释:
1)会将当前处理器缓存行的数据立即写回到系统内存;
2)这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)。
五、Volatile可见性、原子性与有序性
- 并发编程三大特性:可见性、原子性、有序性。
- volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制。
下面我们来看一道阿里的面试题
public class VolatileActomicTest {
private static volatile int num = 0;
public static void increase(){
num++;
}
public static void main(String[] args) throws InterruptedException{
Thread[] threads = new Thread[2];
for(int i = 0; i < 2; i++){
threads[i] = new Thread(new Runnable() {
public void run() {
for(int i = 0; i < 1000; i++){
increase();
}
}
});
threads[i].start();
}
for(Thread t : threads){
t.join();
}
System.out.println(num);
}
}
以上是代码及运行结果,从结果我们可以看出运行结果是小于等于2000,证明了volatile不能保证原子性。
接着我们通过这张内存模型的图片对这道程序进行分析。在理想情况下,我们会以为结果就是2000,启动两个线程,每个线程分别增加1000;但是这些assign等数据原子操作可能存在两个线程同时进行同一操作,或者先后操作的情况。当线程1和线程2都use变量进行加1操作,并且都assign回工作内存后,线程1先进行了store操作,此时通过嗅探机制监听,线程2的工作内存中的num会被置为无效。接着,线程2会重新去主内存读取num的值(线程1已经将num=1write到主内存),接着线程2再根据取到的值再次进行循环自增的操作。到目前为止,线程1进行了一次自增,线程2进行了两次自增,但此时num的值是2,而不是3。这就是为什么程序的执行结果大多数时候不是2000而是小于2000的原因。