这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战
volatile是Java虚拟机提供的最轻量级的同步机制
-
直观上的作用
-
并发可见性
-
防止指令重排序
-
-
实现原理
-
可见性保证
-
为什么并发存在不可见?
-
因为普遍的CPU三级缓存结构,java定义了JMM来屏蔽底层硬件和操作系统交互的细节。
-
JMM中Java线程工作的时候分为主内存和工作内存。线程使用变量的时候都会将主内存的数据拷贝到工作内存中,操作完成后写回主内存。这就存在并发的安全问题。
-
比如线程循环一个变量,这个变量就会一直读取工作内存的值。外部其他线程修改该值,当前线程是无法感知的。
-
-
怎么实现可见的?
-
简单说:加了volatile的变量,在写操作的时候会将数据强制刷新到内存中,并让其他线程的工作内存失效。
-
具体实现:
-
\1. 加了volatile的变量在编译到汇编层的时候,会在volatile变量的操作指令前加上一个lock前缀。这个前缀是个汇编指令。加了该指令当该变量发生变化的时候,就会立即被写回到主存中。
-
\2. 在变量写回到主存中,数据都将是通过总线拷贝的。借助于总线嗅探的技术,当一个线程对变量修改后,加了volatile的数据会强制被刷到主内存中,这刷回的过程需要走总线,而该操作就可以被其他的工作线程感知到,然后工作内存就会将该内存置为失效。再使用的时候,就需要从主内存重新获取。
-
volatile的可见性原理其实相当于本身就是一套缓存一致性协议机制,底层来支持这套一致性协议的实现有很多,不同处理器可能不同。比较典型的就是Intel的MESI缓存一致性协议。当然在老的处理器中,还有锁总线的方式!
-
最重要的就是这lock指令:他会让该变量的主存和缓存强制同步,也相当于一个内存屏障来保证下面的有序性。
-
-
-
和MESI的关系
-
这个工作内存和主内存的工作方式其实很像 MESI。其实部分底层实现也是借助于MESI的,但是有些CPU架构中是没有MESI的,那么volatile就得通过其他的方式实现了。这也是为什么有MESI了,还需要volatile的原因之一。
-
MESI协议可以是保证cache一致性,Java为什么还需要volatile?
-
MESI只是保证了缓存一致性,重排序无法保证。
-
MESI只是intel的部分处理器有,但是volatile实现可见性是不分平台的,是抽象层的一致性。所以还是需要volatile。
-
CPU不仅仅只是三级缓存,在L1和CPU之间还可能有store buffer/invalid queue这些存储,这些可见性的保证,还需要volatile结合其他的机制来实现。
-
-
-
总线风暴
- 当有大量的volatile 和cas 进行数据修改的时候就会产大量嗅探消息。这样就大量占据的总线的带宽。所以不要大量使用Volatile!
-
-
有序性保证
-
为什么会乱序
-
编译器和CPU都会对指令进行优化。不过他们都会准守as-if-serial原则(就是执行的语义不变,就像在单线程下执行一样)
-
一旦乱序,在并发的情况下会发生比如:还如单例模式里面,因为乱序,可能单例对象还未初始化就被外部引用。
-
所以保证有序的主要目的就是:重要的变量在没有写完之前,不能让别人读!
-
-
怎么保证不乱序
-
简单说:加了volatile的变量,在指令生成字节码的时候,会在对应的指令序列中加入内存屏障指令。
-
在写volatile的前加上:StoreStore屏障。(禁止前后的写操作和其重排序) 后加上StoreLoad。(禁止后面的读写和该写重排序)
-
在读volatile变量的后面加上:LoadLoad(禁止后面的读和其重排序),LoadStore屏障(禁止后面的写和其重排序)。
-
-
happens-before
- JDK5之后,提出了这个概念,定义了一系列的原则。在这些原则场景下,对一些编译器优化导致的指令重排序操作进行禁止。即A happens-before B,那么A的操作必须发生在B之前。
-
-
-
注意事项:
-
volatile不能乱加!
- volatile缓存的可见是针对整个缓存行cacheLine的可见。 如果volatile加的比如是一个数组,或者是一个cacheLine中会被多个线程频繁修改的变量。那么就会导致频繁的刷新缓存,就会导致效率很慢。(这就是伪共享)
-
伪共享
-
CPU每次缓存的不是一个变量的值,而是一个cacheLine。一个cacheLine一般是 64 个字节。
-
这个cacheLine中可能变量a和b,分别被不同的线程操作,如果a被其中一个线程修改了,那么会让其他线程的缓存全部失效。其他线程读取b的时候就需要重新刷新缓存。
-
避免伪共享:
-
字节填充: 一个cacheLine64字节,那么就在对应变量后面补充对应字节,比如long类型8字节,就在后面再申明7个long类型变量。
-
@ sun.misc.Contended 注解,配置后,可以自动帮我们进行缓存填充。
-
-
-
-
总结
-
volatile保证了可见性和有序性
-
可见性:JMM设定及缓存一致性协议(JMM的8个操作知识实现可见性的借助的操作指令,可见性保证和他们本质无关)
-
有序性:内存屏障
-
-
缺点:总线风暴,伪共享效率降低。
-