volatile是JMM的一种具体实现方式,是一种轻量级的synchronized,用于修饰共享变量,在多线程环境下能够保证原子性、可见性、有序性。
volatile修饰的变量如何保证在其被修改之后,能够被其他线程立马得到修改之后的值?
当CPU写数据的时候发现这个变量是共享变量的时候,会发出信号通知(总线嗅探)其他CPU将该变量的缓存设置成无效状态,因此当其他CPU需要读取的时候,发现当前变量的缓存行被设置成无效了,自然会到主内存中读取最新的变量值。(那么其他CPU如何发现数据是否无效呢?)
每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,如果过期了,则会将自己的处理器缓存行设置成无效状态,等下次处理器去读取缓存行的时候会发现缓存过期,自然去主内存中读取最新的数据。
由于volatile的缓存一致性,需要其他CPU不断地从主内存嗅探,无效的交互会导致总线带宽达到峰值,因此不要大量使用volatile。
为什么会重排序?
因为在程序执行时,为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令的重排序,达到最优的执行性能。
Java编译器会在生成指令系列时的适当位置中加入内存屏障,来阻止处理器的重排序。以下是四种内存屏障。
为什么会重排序呢,因为编译器和指令器会对既定的代码进行乱序处理进而提升效率,但是是有个规则的,不能什么都给你乱排序,因此JDK 5开始引入了happends-before这个规则来规定哪些操作是不可以排序的。
其中就涉及到了volatile变量。 volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对改变量的读。
其中比较有印象的是锁定规则、volatile、传递规则、线程启动规则等等。
volatile其实是遵守as-if-serial语义,也就是说不管怎么重排序,单线程程序所执行的结果是不会改变的。如果操作之间存在着依赖关系,这时候是不会重排序的,但是如果不存在依赖关系,那么编译器和执行器就可以进行指令重排序,但是无论怎么重排序,它跟单线程程序所执行的结果都是一样。
比较经典的例子就是i++这种问题,需要明白的是i++不是原子操作,i++包括了三步操作:
从主内存读取i的值到工作内存中。从工作内存中读取修改i的变量+1,并刷回到工作内存当中。线程会找时间把工作内存中的变量刷回到主内存当中。现在举例来说明为什么不能解决i++问题,现在比如有两个线程,线程a从主内存中读取到了i的变量到工作内存当中,同时线程b也从主内存中读取到i的变量到工作内存中,都是100。此时线程a和线程b分别对各自工作内存的i变量进行+1,此时能触发vaolatile的可见性吗?很明显是不行的,因为并没有刷回到主内存当中触发总线嗅探机制。重点来了,这个时候线程a和线程b中的变量i值都是101,线程b把变量i刷回主内存当中,很明会触发总线嗅探机制和缓存一致性协议,线程a会重新读取变量i的值,但是发现此时变量的i值跟现在一模一样,所以不用改变。等线程a一刷新回主存,问题就来了。
详情请看单例模式章节
volatile属于轻量级synchronized,读和写都是无锁的,能保证可见性和有序性,但并不能保证原子性,无法取代synchronized。
volatile只能修饰于变量。
volatile提供了happens-before保证,对volatile变量的写happens-before所有其他线程后续对该变量的读操作。
volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。
volatile适用于做触发器的场景,某个属性被多个线程共享,其中一个线程修改了变量值之后,其他线程能够立马得到值,比如boolean flag做触发器使用
https://mp.weixin.qq.com/s/Oa3tcfAFO9IgsbE22C5TEg
https://juejin.im/post/6844903865343541261#heading-5
https://blog.csdn.net/u010571316/article/details/64906481