volatile:可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
多核多线程下volatile关键字特点:
1、是一种轻量级锁
2、保证被volatile修饰的变量能被所有线程可见的
3、禁止指令重排序,保障有序性
4、volatile只保证从主内存加载到工作内存中的值是最新的,并不保证处理器在进行use、assign操作的共享变量是最新的,因此不支持原子性
/*
在CPU多核处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个CPU CORE通过嗅探机制在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
*/
可见性原理通过如下的例子说明:
如volatile修饰共享变量flag,在汇编语言级别flag变量会带有lock前缀指令。当其中一个CPU CORE修改缓存中的flag变量后,如果发现其带有lock前缀指令,这时CPU会马上发起lock操作,锁定主内存中的flag变量,并将新的值回写主内存,然后unlock。在回写到主内存过程中,如果缓存一致性协议是开启的,那么在回写数据在通过总线时,CPU通过嗅探机制会感知的共享变量flag发生了改变,就会把工作内存(即缓存)中的共享变量副本做失效处理。待主内存的值修改完unlock后,CPU在重新读取新的值到工作内存。
高速缓存一致性协议(MESI):也称缓存一致性,是指在采用层次结构存储系统的计算机系统中,保证高速缓冲存储器中数据与主存储器中数据相同机制。在一个系统中,当许多不同的设备共享一个共同存储器资源,在高速缓存中的数据不一致,就会产生问题。
lock指令作用
1、将当前处理器缓存行中的数据立即写回主内存
2、数据写会主内存的过程中,CPU通过嗅探机制感知到,带有lock前缀指令的数据发生变化,会使其它CPU缓存的此数据失效。
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
happens-before:原则用来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据。
- 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
- 锁规则解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
- 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
- 传递性 A先于B ,B先于C 那么A必然先于C
- 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
- 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
- 对象终结规则对象的构造函数执行,结束先于finalize()方法内存屏障。
volatile禁止重排序原理是通过内存屏障来完成的。
指令重排序:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果一致,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
内存屏障:又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
内存屏障从两层来看:
一、CPU硬件原语级别:
- lfence,是一种Load Barrier 读屏障
- sfence, 是一种Store Barrier 写屏障
- mfence, 是一种全能型的屏障,具备ifence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁
二、JVM提供的内存屏障指令
volatile内存语义的实现,JMM针对编译器制定的volatile重排序规则表如下: