volatile的作用
- volatile的作用是保证共享变量的可见性,不能保证原子性,也不能保证线程安全。
- volatile的作用是确保所有线程在同一时刻读取到的共享变量的值是一致的。
- 如果某个线程对volatile修饰的共享变量进行修改,那么其他线程可以立刻看到这个更新。
硬件系统架构
CPU首先使用自己的寄存器,然后使用速度更快的L1缓存,其中L1D缓存数据,L1I缓存指令。
L1缓存和次快的L2做同步数据;L2缓存和L3缓存做同步数据;L3缓存和主内存同步数据。
直写(write-through)
直写是透过本级缓存,直接把数据写到下级缓存(或直接到内存)中,如果对应数据被缓存了,我们同时更新缓存中的内容(甚至直接丢弃)。所以直写时缓存行永远和它对应的内存内容匹配。
回写(write-back)
缓存不会立刻把写操作传递到下一级,而是仅修改本级缓存中的数据,并且把对应的缓存数据标记为“脏“数据。脏数据会触发回写,也就是把对应的内容写到对应的内存或下一级缓存中。回写后,脏数据又变干净了。当一个脏数据被丢弃时,总是要先进行一次回写。
缓存一致性协议
解决缓存一致性方案有两种:
- 通过总线加LOCK#锁的方式;
- 通过缓存一致性协议。
方案一存在一个问题,它是采用一种独占的方式来实现,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。
【窥探技术+MESI协议】的出现,就是为了解决多核CPU时代缓存不一致的问题
窥探技术
- ”窥探“背后的基本思想是,所有的数据传输都发生在一条共享的总线上,所有CPU都能看到这条总线。
- 缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(arbitrate):同一个指令周期中,只有一个缓存可以读写内存。
- 窥探技术的思想是,缓存不仅仅在做内存传输的时候才和总线打交道,而是不停的窥探总线上发生的数据交换,跟踪其他缓存在做什么。
- 所以当一个缓存代表它所属的CPU去读写内存时,其他CPU都会得到通知,它们以此来使自己的缓存保持同步。只要某个CPU一写内存,其他CPU马上就会知道这块内存在它们自己的缓存中对应的缓存行已经失效。
MESI协议
缓存系统操作的最小单位是缓存行,而MESI是缓存行四种状态的首写字母,任何多核系统中的缓存行都处于这四种状态之一。
- 失效(Invalid)缓存行:该CPU缓存中无该缓存行,或缓存中的缓存行已经失效;
- 共享(Shared)缓存行:缓存行的内容是同主内存内容保持一致的一份拷贝。在这种状态下的缓存行只能被读取不能被写入。多组缓存可以同时拥有针对同一内存地址的共享缓存行;
- 独占(Exclusive)缓存行:和S状态一样,也是和主内存内容保持一致的一份拷贝。区别在于,如果一个CPU持有了某个E状态的缓存行,那其他CPU就不能同时持该内容的缓存行,所以叫独占。这意味着,如果其他CPU原本也持有同一缓存行,那么它会马上变为失效状态。
- 已修改(Modified)缓存行:该缓存行已经被所属的CPU修改了。如果一个缓存行处于已修改状态,那么它在其他CPU缓存中的拷贝马上会变成失效状态。此外,已修改缓存行如果被丢弃或标记为失效,那么先要把它的内容回写到内存中。
只有当缓存行处于E和M状态时,CPU才能去写它,也就是说只有这两种状态下,CPU时独占这个缓存行的;
当CPU想写某个缓存时,如果它没有独占权,它必须先发一条我要独占权的请求给总线,这会通知其他CPU,把它们拥有的同一缓存行的拷贝失效;
只有在获取独占权后,CPU才能开始修改数据。并且此时,这个CPU知道,这个缓存行只有一份拷贝,在我自己的缓存里,所以不会有任何冲突;
反之,如果有其他CPU想读取这个缓存行,独占或已修改状态的缓存行必须先回到共享状态。
as-if-serial
数据依赖性
如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性。数据依赖分下列三种类型:
名称 | 代码示例 | 说明 |
写后读 | a = 1;b = a; | 写一个变量后,再读这个变量 |
写后写 | a = 1;a = 2; | 写一个变量后,再写这个变量 |
读后写 | a = b;a = 1; | 都一个变量后,再写这个变量 |
as-if-serial
上面三种情况,只要重排两个操作的执行顺序,程序的执行结果将会被改变。
编译器和处理器可能会对操作做重排序。
编译器和处理器在重排序时,会遵守数据依赖性,
编译器和处理器不会改变存在数据依关系的两个操作的执行顺序。
as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器运行时和处理器都必须遵守as-if-serial。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
as-if-serial语义把单线程 程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。
as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
指令重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,重排序分为三种类型:
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
volatile
volatile内存语义
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。
在JVM底层volatile是采用“内存屏障”来实现的。
上述主要表明1.保证可见性,不保证原子性;2.禁止指令重排序。
可见性问题主要一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的工作内存。
volatile关键字能有效的解决这个问题。
java中的happen-before规则中对Happen-before的定义如下:
- 程序顺序规则:一个线程中的每个操作,happen-before于该线程中的任意后续操作;
- 监视器锁规则:对一个监视器的解锁,happen-before于随后对这个监视器的枷锁;
- volatile变量规则:对一个volatile域的写,happen-before于任意后续对这个volatile域的读;
- 传递性:如果A happen-before于 B,且B happen-before C,那么A happen-before C;
- 线程的start()方法happen-before 该线程所有的后续操作;
- 线程所有的操作happen-before其他线程在该线程上调用join返回成功后的操作。
volatile原理
为了实现volatile可见性和happen-before的语义。JVM底层是通过一个叫“内存屏障”的东西来完成。
内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。
下面是完成上述要求的一些规则,在NO的地方就是需要用内存屏障来控制的。
是否可以重排序 | 第二个操作 | |||
第一个操作 | 普通读 | 普通写 | volatile读 | volatile写 |
普通读 | NO | |||
普通写 | NO | |||
volatile读 | NO | NO | NO | NO |
volatile写 | NO | NO |
是否可以重排序 | 第二个操作 | |||
第一个操作 | 普通读 | 普通写 | volatile读 | volatile写 |
普通读 | LoadStore | |||
普通写 | StoreStore | |||
volatile读 | LoadLoad | LoadStore | LoadLoad | LoadStore |
volatile写 | StoreLoad | StoreStore |
屏障类型 | 指令示例 | 说明 |
LoadLoad Barriers | Load1; LoadLoad; Load2; | 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载 |
StoreStore Barriers | Store1; StoreStore; Store2; | 确保Store1数据对其他处理器可见(刷新到内存),之前与Store2及所有后续存储指令的存储 |
LoadStore Barriers | Load; LoadStore; Store; | 确保Load数据装载,之前于Store及所有后续的存储指令刷新到内存 |
StoreLoad Barriers | Store; StoreLoad; Load; | 确保Store数据对其他处理器变得可见(刷新到内存),之前于Load及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 |