1.硬件内存模型
1.1CPU缓存结构
程序是指令与数据的集合,计算机执行程序时,是C P U
在执行每条指令,因为C P U
要从内存读指令,又要根据指令指示去内存读写数据做运算,所以执行指令就免不了与内存打交道,早期内存读写速度与C P U
处理速度差距不大,倒没什么问题。
随着C P U
技术快速发展,C P U
的速度越来越快,内存却没有太大的变化,导致内存的读写(IO
)速度与C P U
的处理速度差距越来越大,为了解决这个问题,引入了缓存(Cache
)的设计,在C P U
与内存之间加上缓存层,这里的缓存层就是指C P U
内的寄存器与高速缓存(L1,L2,L3
)
图中可以看出离C P U
越近,存储空间越大速度越慢
1.2 CPU 缓存与内存交互
C P U
运行时,会将指令与数据从主存复制到缓存层,后续的读写与运算都是基于缓存层的指令与数据,运算结束后,再将结果从缓存层写回主存。C P U
基本都是在和缓存层打交道,采用缓存设计弥补主存与C P U
处理速度的差距,这种设计不仅仅体现在硬件层面,在日常开发中,那些并发量高的业务场景都能看到,但是凡事都有利弊,缓存虽然加快了速度,同样也带来了在多线程场景存在的缓存一致性问题。
1.3内存屏障 Memory Barrier(Memory Fence)
(volatile 原理是依据内存屏障的)
-
可见性
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
-
有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
2.JAVA内存模型
J M M
是建立在硬件内存模型基础上的抽象模型,并不是物理上的内存划分,简单说,为了使Java
虚拟机(Java Virtual Machine,J V M
)在各平台下达到一致的内存交互效果,需要屏蔽下游不同硬件模型的交互差异,统一规范,为上游提供统一的使用接口。J M M
是保证J V M
在各平台下对计算机内存的交互都能保证效果一致的机制及规范。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分),
每条线程还有自己
的工作内存(Working Memory,可与前面讲的处理器高速缓存类比
),线程的工作内存中保 存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内 存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量值的传递均需要通过主内存来完成。
J M M
抽象结构划分为线程本地缓存与主存,每个线程均有自己的本地缓存,本地缓存是线程私有的,主存则是计算机内存,它是共享的。
2.1可见性
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,这就是可见性,如果无法保证,就会出现缓存一致性的问题,J M M
规定,所有的变量都放在主存中,当线程使用变量时,先从缓存中获取,缓存未命中,再从主存复制到缓存,最终导致线程操作的都是自己缓存中的变量。
缓存一致性问题举例子:
A
、B
两个线程执行完后,线程A
与线程B
缓存数据不一致,这就是缓存一致性问题,一个是1
,另一个是2
,如果线程A
再进行一次+1
操作,写入主存的还是2
,也就是说两个线程对a
共进行了3
次+1
,期望的结果是3
,最终得到的结果却是2
。
解决缓存一致性问题,就要保证可见性:变量写入主存后,把其他线程缓存的该变量清空,这样其他线程缓存未命中,就会去主存加载。
A
、B
两个线程执行完后,线程A
缓存是空的,此时线程A再进行一次+1
操作,会从主存加载(先从缓存中获取,缓存未命中,再从主存复制到缓存)得到2
,最后写入主存的是3
,Java
中提供了volatile
修饰变量保证可见性。看似问题都解决了,然而上面描述的场景是建立在理想情况(线程有序的执行),实际中线程可能是并发(交替执行),也可能是并行,只保证可见性仍然会有问题,所以还需要保证原子性。
2.2原子性
原子性是指一个或者多个操作在C P U
执行的过程中不被中断的特性,要么执行,要不执行,不能执行到一半。
int a=0;//原子性操作:int a=0只有一步操作,就是赋值
a++;//非原子操作:a++有三步操作,读取值、计算、赋值
如果多线程场景进行a++
操作,仅保证可见性,没有保证原子性,同样会出现问题。
为了解决此问题,只要把多个操作变成一步操作,即保证原子性。
Java
中提供了synchronized
(同时满足有序性、原子性、可见性)可以保证结果的原子性(注意这里的描述),因为synchronized
可以对代码片段上锁,防止多个线程并发执行同一段代码。
2.3有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,这种叫做指令重排
重排遵循as-if-serial
原则,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果(即不管怎么重排序,单线程程序的执行结果不能被改变),下面这种情况,就属于数据依赖。
int i = 10
int j = 10
//这就是数据依赖,int i 与 int j 不能排到 int c下面去
int c = i + j
但也仅仅只是针对单线程,多线程场景可没这种保证,假设A、B
两个线程,线程A
代码段无数据依赖,线程B
依赖线程A
的结果,如下图(假设保证了可见性).
为解决重排序,使用Java提供的volatile
修饰变量同时保证可见性、有序性,被volatile
修饰的变量会加上内存屏障禁止排序
3.深入解析volatile关键字
volatile内存屏障