Java内存模型
Java内存模型应该说成Java线程内存模型,同CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的屏蔽掉了底层计算机的区别。
如上图所示每一个线程对应一个工作内存,相当于cpu的高速缓存,从主内存中获取共享变量,并存贮一份共享变量副本,而每个线程的共享变量副本都是相对独立的,那么当我们写多线程程序的时候,当不了解Java线程内存模型的时候,并发一上来就会出现这种bug,我们来看下面这个程序。
private static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println("======>wait data");
while(!initFlag){
}
System.out.println("==============>success");
}).start();
Thread.sleep(2000);
new Thread(()->{
prepareData();
}).start();
}
public static void prepareData(){
System.out.println("==============>prepareData start");
initFlag = true;
System.out.println("==============>prepareData end");
}
这个程序线程1是执行一个循环,当initFlag值不变的时候,会一直循环。线程2是执行修改这个initFlag这个值,那么理论上来说这个程序的数据结果应该是:
======>wait data
==============>prepareData start
==============>prepareData end
==============>success
实际上,并不是这样的,它会陷入一个死循环,success不会输出出来,按理说,线程一应该可以感受的到initFlag值的变化,实际上线程1一直读取的是,工作内存中的共享变量副本。
先了解一下JMM的原子操作:
read(读取):从主内存中读取数据
load(载入):将主内存中读取到的数据写入工作内存中
use(使用):从工作内存中读取数据来计算
assign(赋值):将计算好的值重新赋值到工作内存中
store(存储):将工作内存数据写入主内存中
write(写入):将store过去的变量赋值给主内存中的变量
lock(锁定):将主存变量加锁,标识为线程独占状态
unlock(解锁):将主存变量解锁,解锁后其他线程可以锁定该变量。
那么将上述程序结合JMM的原子操作分析如下图所示:
首先线程1先从主内存读取initFlag变量然后load到工作内存中后use--->!initFlag变量,此时工作内存中,线程2也从主内存中read-》initFlag变量然后load到工作内存中,use变量并assign到工作内存中,然后store将数据写入主内存中,然后再write到主内存中,此时线程1读取的还是工作内存中的值,导致值没有一致性,那么如何解决这个问题呢?
在initFlag变量加上volatile关键字:
private static volatile boolean initFlag = false;
众所周知volatile可以使变量缓存可见性,并且禁止指令重排,那么它底层是如何实现的呢?底层实现主要是通过汇编loca前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)
IA-32架构软件开发手册对lock指令解释:
1)、会将当前处理器缓存行的数据立即写回到系统内存
2)、这个写会内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)
加了volatile关键字之后所对应JMM的原子操作操作如图所示:
比较所不同之处:在store到主内存的时候,因为MESI缓存一致性协议的存在,其他CPU里有一个总线嗅探机制,可以理解为监听,该内存地址的数据改变则置为无效,所以当再一次执行!initFlag指令时,会重新读取载入数据,则获取的时true,从而可以循环结束,输出success,而如果两个线程同时修改initFlag时,一个线程再修改时会有缓存行级锁,所以需要等待释放锁之后再进行后续操作,这样的话读取使保持了数据的一致性,又不会影响性能,比直接再主内存加总线锁要轻量级很多。
但是这样又会暴露其他问题,再看以下代码:
private static volatile int num = 0;
public static void increate(){
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for(int i = 0 ; i<=threads.length;i++ ){
threads[i] = new Thread(()->{
for(int j = 0; j< 1000 ;j++){
increate();
}
});
threads[i].start();
}
for(Thread t:threads){
t.join();
}
System.out.println(num);
}
上述程序理论上应该输出:10*1000=10000,实际上呢结果不一,可能是10000也是9888,这是为什么呢?按找上述描述加了volatile关键字有缓存行级锁,那么取值应该是对的,问题就出在这里,线程1在执行num++的赋值之后store的时候有一个过程,线程2在store之前也执行了++操作,然后再线程一store write之后,由于cpu嗅探机制所以线程二的数据失效重新获取,再操作一次++的时候就不是原子操作了,所以导致没有达到预期的值,如图所示:
那么可以再++方法上加锁保持操作的原子性,这里就不讨论加锁及其细节了。
并发编程三个概念:
1、原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
2、可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3、有序性:即程序执行的顺序按照代码的先后顺序执行。