volatile 关键字的核心知识点,要关系到 Java 内存模型(JMM,Java Memory Model)上。虽然 JMM 只是 Java 虚拟机这个进程级虚拟机里的一个内存模型,但是这个内存模型,和计算机组成里的 CPU、高速缓存和主内存组合在一起的硬件体系非常相似。理解了 JMM, 可以让你很容易理解计算机组成里 CPU、高速缓存和主内存之间的关系。
1.为什么会出现可见性问题?
事实上,我们可以把 Java 内存模型和计算机组成里的 CPU 结构对照起来看。
我们现在用的 Intel CPU,通常都是多核的的。每一个 CPU 核里面,都有独立属于自己的 L1、L2 的 Cache,然后再有多个 CPU 核共用的 L3 的 Cache、主内存。
因为 CPU Cache 的访问速度要比主内存快很多,而在 CPU Cache 里面,L1/L2 的 Cache 也要比 L3 的 Cache 快。所以,CPU 始终都是尽可能地从 CPU Cache 中去获取数据,而不是每一次都要从主内存里面去读取数据。
这个层级结构,就好像我们在 Java 内存模型里面,每一个线程都有属于自己的线程栈。线程在读取 COUNTER 的数据的时候,其实是从本地的线程栈的 Cache 副本里面读取数据, 而不是从主内存里面读取数据。
2.JMM 实现 volatile:内存屏障
PS:关于 JMM 可以参考这篇文章…JMM 的结构与计算机硬件很相似(虚拟机内存-内存、线程工作空间-高速缓存、线程-CPU),但是更重要的一点,JMM 的必要性在于规范工作空间的数据与内存数据的交互(可见性、有序性)!!
- 可见性:线程只能操作自己工作空间中的数据 => volatile 关键字
- 有序性:为了提高执行效率,会有编译重排序和指令重排序,所以程序中的顺序不一定就是执行的顺序
- 线程内:as-if-seria,单线程中重排序后不影响执行结果
- 多线程:happens-before 规则
在 JMM 中,其实是通过内存屏障实现了 volatile,在保证可见性的同时,又保证了有序性
1)内存屏障作用
- 阻止屏障两侧指令重排序
- 强制把写修改的数据写回主存(Store)+ 其余缓存中相应的数据失效(Load)=>别的线程再读时需要到主存中重新读取
PS:来看一下保证有序性的 happens-before 规则:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程任意后续操作
- start()规则:如果线程A执行操作threadB.start(),那么A线程中threadB.start()happens-beforeB的任意操作
- join()规则:如果线程A执行操作thread.join(),那么线程B的任意操作happens-before于A从threadB.join()返回
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
- 监视器锁规则:对一个锁(synchronized)的解锁,happens-before于随后对这个锁的解锁
- 传递性:如果A happens-before B,B happens-before C ,那么 A happens-before C
2)内存屏障分类
-
Store:更新主存
-
Load:让高速缓存失效,强行刷新
- LoadLoad:volatile读之后,避免volatile读操作和后面普通的读操作进行重排序
- StoreStore:volatile写之前,禁止上面的普通写与后面的volatile写重排序
- LoadStore:volatile读之后,避免volatile读操作和后面普通的写操作进行重排序
- StoreLoad:volatile写之后,避免volatile写操作与后面可能存在的volatile读写操作发生重排序