Skip to content

Latest commit

 

History

History
84 lines (55 loc) · 3.56 KB

volatile.md

File metadata and controls

84 lines (55 loc) · 3.56 KB

保证线程可见性

当一个变量被volatile修饰之后,可以保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需通过主内存来完成。

volatile的保证线程可见性的底层是通过CPU的缓存一致性协议来完成的,这里只是简单介绍下缓存一致性协议,不去深追CPU里面的各种原语。

MESI-缓存一致性协议

概念

MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议。

MESI协议中的状态

CPU中每个缓存行cache line(64bytes)使用4种状态进行标记(使用额外的两位(bit)表示):

Modified

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的,即与主存中的数据不一致。

该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取主存中相应内存之前)写回主存。

当被写回主存之后,该缓存行的状态会变成Exclusive状态。

Exclusive

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的,与主存中数据一致。

该状态可以在任何时刻当有其它CPU读取该内存时变成Shared状态。

同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

Shared

该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致。

当有一个CPU修改该缓存行中的数据时,其它CPU中该缓存被变成Invalid状态。

Invalid

该缓存是无效的(可能有其它CPU修改了该缓存行)。


禁止指令重排序

  1. 字节码层面 ACC_VOLATILE
  2. JVM层面,底层用了4个内存屏障来实现禁止指令重排序。

StoreStoreBarrier

volatile 写操作

StoreLoadBarrier

LoadLoadBarrier

volatile 读操作

LoadStoreBarrier

实例请参考单例设计模式中的DCL(Double Check Lock)


为什么volatile不能保证原子性

一个变量i=100volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值刷新到主存中。

首先线程A读取了i的变量的值,这个时候线程切换到了B,线程B同样从主内存中读取i的值,由于线程A没有对i做过任何修改,此时线程B获取到的i仍然是100。线程B工作内存中为i执行了加1的操作,但是没有刷新到主内存中,这个时候又切换到了A线程,A线程直接对工作内存中的100进行加1操作(因为A线程已经读取过i的值了),由于线程B并未写入i的最新值,这个时候A线程的工内存中的100不会失效。 最后,线程Ai=101写入主内存中,线程B也将i=101写入主内存中。 始终需要记住,i++的操作是3步骤!


适用场景

volatile最适用一个线程写,多个线程读的场合。

如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。(摘自Netty权威指南)


参考资料

  • 《深入理解Java虚拟机第3版》
  • 《Java并发编程的艺术》