为什么volatile不具有原子性
volatile为什么不能保证原子性呢?一般都拿i++这种自增来证明volatile不具备原子性。
网上的答案各种复制粘贴,我想说说我的想法。
为什么volatile不具有原子性。
volatile方式的i++,总共是四个步骤:
i++实际为load、Increment、store、Memory Barriers 四个操作。
内存屏障是线程安全的,但是内存屏障之前的指令并不是.在某一时刻线程A将i的值load取出来,放置到cpu缓存中,然后再将此值放置到寄存器A中,然后A中的值自增1(寄存器A中保存的是中间值,没有直接修改i,因此其他线程并不会获取到这个自增1的值)。如果在此时线程B也执行同样的操作,获取值i10,自增1变为11,然后马上刷入主内存。此时由于线程B修改了i的值,实时的线程1中的i10的值缓存失效,重新从主内存中读取,变为11。接下来线程1恢复。将自增过后的A寄存器值11赋值给cpu缓存i。这样就出现了线程安全问题。
简单来说就是,对于i++这种非原子性操作,volatile只能保证从主内存中将变量取值到cpu寄存器的时候会保证其强制刷新。但是已经被取到寄存器中的值,不再被检查。
volatile与屏障
volatile在JVM层面上拒绝了编译器来实现的一些重排优化。(所谓的优化屏障)
volatile在汇编层面上通过lock#前缀指令来实现的可见性和有序性。(所谓的 内存屏障)
通过对比加入volatile和未加入volatile关键字所生成的汇编代码就会发现,关键变化在于有volatile修饰的变量,赋值后多执行了一个“lock addl $0x0,(%esp)”操作,这个操作相当于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。这句指令中的“addl $0x0,(%esp)”(把ESP寄存器的值加0)显然是一个空操作(采用这个空操作而不是空操作指令nop是因为IA32手册规定lock前缀不允许配合nop指令使用),关键在于lock前缀,查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU或者别的内核无效化(Invalidate)其Cache,这种操作相当于对Cache中的变量做了一次前面介绍Java内存模式中所说的“store和write”操作。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。
内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,它使得 CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行, 也就是说在内存屏障之前的指令和之后的指令不会由于系统优化等原因而导致乱序。
lock前缀指令相当于就是总线锁。
mesi协议其实与volatile没什么关系。
mesi协议是基于cpu层面的一个黑盒系统。只要cpu支持就一定会实现。
但是mesi协议会导致,当一个核心修改了数据以后,其他核心都要等待这个核心将这个内存值的状态广播出去。
这个时候有了store buffer和invalid queue来做一些异步的优化。
这时候就需要volatile来保证缓存的强一致性(总线锁的手段。)
所以会导致总线风暴。
什么情况下需要使用volatile
当这个变量的赋值与原值没关系的时候。也可以说成你不在乎volatile的值是什么,你只在乎他有没有变化的场景。