1.内存模型(JMM)
1.1什么是Java内存模型?
Java内存模型将内存分为主内存和工作内存两大部分;主内存用来存储线程之间共享数据,工作内存则是每个线程独享内存,存储对应线程使用到的共享数据副本
Java内存模型和硬件中的多核CPU、CPU对应的缓存(1级缓存、2级缓存、3级缓存)、以及内存之间的设计异曲同工;CPU的每个核可以跑不通的线程,由于CPU执行指令速度远远大于内存读写速度,为了平衡两者差距,高速缓存就引入了计算机体系中,高速缓存位于CPU和内存之间;读写速度能力 CPU>L1>L2>L3>Memory(越靠近CPU侧,处理能力越高,读写越快);存储数据能力Memory>L3>L2>L1>CPU(越靠近Memory侧,存储能力越强);计算机执行程序时,CPU执行程序时,会频繁的使用数据从内存中拷贝到缓存中,这样CPU运算时可以更快的读取到运算的数据;当CPU运算结束,又将数据从高速缓存写回到内存中。
1.2.内存模型中工作内存和主内存的区别?
- 主内存:所有线程共享的区域;存储共享数据的区域
- 工作内存:线程私有的存储区域;主要存储线程使用的主内存中数据的副本
1.3.内存模型带来什么问题
工作内存和主内存中数据不一致问题
-
原子性
线程1 和 线程2 并发执行 +1操作,并同步到主内存中
-
可见性
- 重排序
public class Test{
private int a = 0;
private int b = 0;
public void setAAndB() {
a = 1;
b = 2;
}
public void changeB() {
if (a == 1) {
b = 4;
}
System.out.println("b=" + b);
}
}
1.4.volatile关键字有哪些语义
volatile主要面对JMM带来的问题(工作内存和主内存中数据不一致)而产生的一种解决方案
语义有
- volatile修饰的变量具有可见性
- volatile修饰的变量读写原子性(如32位系统上的Long变量)
- volatile修饰的变量,读写操作不会参与前后指令的重排序
具体理解
-
volatile修饰的变量具有可见性
对于一个volatile变量的读,总能看到(任意线程)对这个volatile变量最后的写入值
写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中
读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,重新从主内存中读取共享变量到工作内存中
-
volatile修饰的变量读写原子性(单步操作)
对于任意单个volatile变量的读、写具有原子性;但是对于i++这种复合操作不具有原子性
-
volatile修饰的变量,读写操作不会参与前后指令的重排序
是否可以重排序 第二个操作 第二个操作 第二个操作 第一个操作 普通读/写 volatile读 volatile写 普通读/写 不可以重排序 volatile读 不可以重排序 不可以重排序 不可以重排序 volatile写 不可以重排序 不可以重排序 总结:
- 第一个操作为volatile读时,不管第二个操作是什么,都不可以重排序
- 第二个操作为volatile写时,不管第一个操作是什么,都不可以重排序
- 第一个操作为volatile写,第二个操作为volatile读时,不可以重排序
1.5.volatile是怎么实现的
还是围绕三个语义来看
-
可见性
在写入volatile变量的时候,JVM会向CPU发送Lock指令,Lock指令具有两个作用
- 将当前工作内存中数据写入到主内存中
- 涉及其他线程对应的CPU核中缓存的数据设置无效(CPU消息一致性协议和嗅探功能),之后读会重新重主内存拉取最新数据
-
原子性(读写原子性)
线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令之间是没有任何关系的。
所以,volatile是不能保证(复合操作)原子性的
-
重排序(有序性)
主要靠内存屏障(硬件层面的指令,它可以禁止屏障前后的指令进行重排序)
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障