多线程学习(三)之volatile关键字
volatile的概述
百度百科:
volatile 是干什么用的呢?
volatile 可以使得在多处理器环境下保证了共享变量的可见性
可见性:
举个例子:在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是 一个很正常的事情。但是在多线程环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是所谓的可见性。
为了实现跨线程写入的内存可见性,必须使用到一些机制来实现。而 volatile 就是这样一种机制
从硬件层面了解可见性的本质
我们知道,CPU的运算速度很快,其次是内存(运存),再次才是我们的硬盘,那么如果仅仅是cpu的运算速度很快的话,内存和硬盘的速度是跟不上cpu的速度。那么这台计算器的性能就是最慢的硬盘的性能。这就是 “木桶效应” 。
为了平衡三者的速度差异,最大化的利用 CPU 提升性能,从硬件、操作系 统、编译器等方面都做出了很多的优化
- CPU 增加了高速缓存
cpu在执行运算任务的时候,少不了和内存交互,读取或者写入数据。而由于计算机的存储设备与CPU的运算速度差距非常大,所以现代计算机系统都会增加一层读写速度尽可能接近CPU运算速度的高速缓存来作为内存 和处理器之间的缓冲;将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中。
- 操作系统增加了进程、线程。通过 CPU 的时间片切换最大化的提升 CPU 的使用率
- 编译器的指令优化,更合理的去利用好 CPU 的高速缓存
高速缓存示意图
通过高速缓存的存储交互很好的解决了处理器与内存的速度差异,但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题,缓存一致性。
缓存一致性
什么叫缓存一致性呢?
首先,有了高速缓存以后,每个 CPU 的处理过程是: 先将计算需要用到的数据缓存在 CPU 高速缓存中,在 CPU 进行计算时,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中。在整个运算过程完成后,再把缓存中的数据同步到主内存。
由于在多 CPU 中,每个线程可能会运行在不同的 CPU 内, 并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个 CPU 中,如果在不同 CPU 中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题
指令重排序
Java语言规范JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
指令重排序的意义:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。
Java Memory Model(JMM)
导致可见性问题的根本原因是缓存以及指令的重排序。 而 JMM 实际上就是提供了合理的禁用缓存以及禁止重排序的方法。所以它最核心的价值在于解决可见性和有序性。
JMM 是如何解决可见性有序性问题的
简单来说,JMM 提供了一些禁用缓存以及进制重排序的方法,来解决可见性和有序性问题。这些方法大家都很熟悉: volatile、synchronized、final;
as-if-serial
不管怎么重排序,对于单个线程来说执行结果不能改变
例如:
int a=2; //1
int b=3; //2
int result=a*b; //3
/*
1 和 3、2 和 3 存在数据依赖,所以在最终执行的指令中,
3 不能重排序到 1 和 2 之前,否则程序会报错。
由于 1 和 2 不存在数据依赖,所以可以重新排列 1 和 2 的顺序
*/
JMM 层面的内存屏障
为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器的重排序, 在 JMM 中把内存屏障分为四类
屏障类型 | 指令示例 | 备注 |
---|---|---|
LoadLoad Barriers | load1; LoadLoad;load2 | 确保load1数据的装载优先于load2及所有后续装载指令的装载 |
StoreStore Barriers | store1; StoreStore; store2 | 确保store1数据对其他处理器可见优先于store2及所有后续存储指令的存储 |
LoadStore Barriers | load1;LoadStore;store2 | 确保load1数据装载优先于store2以及后续的存储指令刷新到内存 |
StoreStore Barriers | store1;StoreStore;load2 | 确保store1 数据对于其他处理器变得可见,优先于load2及所有后续所有装载指令的装载。这条内存屏障是一个全能型的内存屏障 |
HappenBefore
它的意思表示的是前一个操作的结果对于后续操作是可见的,所以它是一种表达多个线程之间对于内存的可见性。 所以我们可以认为在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在 happens-before 关系。这两个操作可以是同一个线程,也可以是不同的线程
- 程序顺序规则;
一个线程中的每个操作,happens-before 于该线程中的任意后续操作; 可以简单认为是 as-if-serial。单个线程中的代码顺序不管怎么变,对于结果来说是不变的.顺序规则表 示:
1 happenns-before 2; 3 happens- before 4
- volatile 变量规则;
对于 volatile 修饰的变量的写的操作, 一定 happen-before 后续对于 volatile 变量的读操作; 根据 volatile 规则,2 happens before 3
- 传递性规则;
如果 1 happens-before 2; 3happens- before 4; 那么传递性规则表示: 1 happens-before 4;
- start 规则;
如果线程 A 执行操作 ThreadB.start(),那么线程 A 的 ThreadB.start()操作 happens-before 线程 B 中的任意操作
- join 规则;
如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。
- 监视器锁的规则;
对一个锁的解锁,happens-before 于 随后对这个锁的加锁