volatile关键字原理
概述
volatile关键字是Java虚拟机提供的最轻量级的同步机制。在多线程编程中volatile和synchronized都起着举足轻重的作用,没有这两者,也就没有那么多JUC供我们使用。
并发编程(线程安全)三大特性
-
原子性:一个操作或者多个操作,要么全部执行成功,要么全部执行失败。满足原子性的操作,中途不可被中断
-
可见性: 多个线程同时访问一个共享变量,当其中一个线程对共享变量进行写操作,其他线程能够立刻知道修改后的值
-
有序性: 程序的执行的顺序按照代码的先后顺序执行,但是JMM模型中允许编译器和处理器为了效率,可能会进行指令重排序的优化,多线程环境下导致乱序,会造成线程不安全的情况,单线程表现为串行.
当开始介绍volatile的时候,我们先看一段代码,这段代码是加了volatile和没加volatile的区别,经过测试发现没加volatile会一直死循环,程序不会中断,而加了volatile之后1秒后程序就会停止运行,那volatile起到了什么作用呢??
/**
* @author 小胡哥哥
* NO BB show your code
*/
public class Test1 {
public /**volatile*/ static boolean isStop = false;
public static void main(String[] args) throws
InterruptedException {
int[] arr = {0};
Thread thread = new Thread(() -> {
while (!isStop) {
arr[0]++;
}
});
thread.start();
System.out.println("开始启动线程");
Thread.sleep(1000);
isStop = true;
}
}
volatile的作用
volatile的可见性
volatile可以在多线程的情况下保证共享变量的可见性,所以上面这段代码,程序没加volatile关键字,主线程和子线程都从主内存拷贝共享数据isStop(false),之后主线程在它的工作内存中将isStop修改为true,但是子线程并不知道isStop共享数据被修改,仍然拿到的时false,所以while一直死循环.(涉及到JMM内存模型,下文会详细介绍)
而加了volatile关键字,主线程和子线程拷贝共享数据到各自线程工作内存中,但是不同的是volatile关键字会先将原先的工作内存进行刷新清除,会强制让其访问主内存的共享数据,所以当主线程对isStop修改为true,放置在主内存中,子线程会被强制刷新,所以子线程能立刻知道主内存的值被修改了,会再次重新向主内存拷贝一份共享数据,所以while不会产生死循环。volatile保证可见性
volatile
定义:
-
当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存
-
写操作会导致其他线程中的缓存无效
volatile变量的禁止指令重排序
volatile是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的。
硬件内存模型
处理器与计算机存储设备运算速度有几个数量级的差别。总不能让处理器总是等待计算机存储设备,这样就没办法显现出处理器的优势。
因此,为了“压榨”处理的性能,达到“高并发”的效果,在处理器和存储设备之间加入了高速缓存(cache)来作为缓冲。
通过高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为 它引入了一个新的问题,缓存一致性。
什么叫缓存一致性呢?
为了达到数据访问的一致,需要各个处理器在访问缓存时 遵循一些协议,在读写时根据协议来操作,常见的协议有MSI,MESI, MOSI 等。 最常见的就是 MESI 协议。 接下来给大家简单讲解一下 MESI.
MESI代表的是四种状态首字母的缩写,其分别是:
-
M(Modify) 表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
-
E(Exclusive) 表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改
-
S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓 存中的数据和主内存数据一致
-
I(Invalid) 表示缓存已经失效 在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听(snoop)其它 Cache 的读写操作
CPU 层面的内存屏障
内存屏障就是将 store bufferes 中的指令写入到内存,从 而使得其他访问同一共享内存的线程的可见性。
X86 的 memory barrier 指令包括 lfence(读屏障) sfence(写屏障) mfence(全屏障)
写屏障(Store Memory Barrier):告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的
读屏障(Load Memory Barrier): 处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
全屏障(Full Memory Barrier):确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作
总的来说,内存屏障的作用可以通过防止 CPU 对内存的乱序访问来保证共享数据在多线程并行执行下的可见性,volatile 关键字的代码,这个关键字会生成一个 Lock 的汇编指令,这个指令其实就相当于实现了一种内存屏障。
JMM内存模型
我们常说的JVM内存模式指的是JVM的内存分区;而Java内存模式是一种虚拟机规范。
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
JMM层面的“内存屏障”:
-
LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
-
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
-
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
-
StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
JMM模型与硬件模型直接的对照关系可简化为下图:
HappenBefore原则
它的意思表示的是前一个操作的结果对于后续操作是可见的,所以它是一种表达多个线程之间对于内存的可见性。 所以我们可以认为在 JMM 中,如果一个操作执行的结果需 要对另一个操作可见,那么这两个操作必须要存在 happens-before 关系。 这两个操作可以是同一个线程,也可以是不同的线程
JMM 中有哪些方法建立 happen-before 规则
**程序顺序规则 **
-
一个线程中的每个操作, happens-before 于该线程中的任意后续操作; 可以简单认为是 as-if-serial。 单个线程中的代码顺序不管怎么变, 对于结果来说是不变的
-
volatile 变量规则,对于 volatile 修饰的变量的写的操作,一定 happen-before 后续对于 volatile 变量的读操作;