volatile特性
内存可见性:通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
volatile的使用场景
通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,但是使用volatile必须满足两个条件:
- 对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的;
- 该变量没有包含在具有其它变量的不变式中。
volatile关键字的使用场景:
-
状态标记量:比如我们要卖票,在高并发的场景下,我们需要知道什么时候可以卖,因此我们可以设置一个boolean值,标记是否可以开始卖票。
public class Sale { private volatile isopen; public void run() { if (isopen) { //开始卖票 } else { //正常逻辑 } } public void setIsopen(boolean isopen) { this.isopen = isopen } }
户的请求线程执行run方法,如果需要开启卖票活动,可以通过后台设置,具体实现可以发送一个请求,调用setIsopen方法并设置isopen为true,由于isopen是volatile修饰的,所以一经修改,其他线程都可以拿到isopen的最新值,用户请求就可以执行卖票了。
-
double check :即双重检查的单例模式。
class Singleton { private volatile static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
如何保证内存可见性?
在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据,下面看看操作普通变量和volatile变量有什么不同:
-
对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。
-
对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。
volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的。内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
我们以上述的单例模式的实现为例,通过观察volatile变量和普通变量所生成的汇编代码可以发现,操作volatile变量会多出一个lock前缀指令:
Java代码: instance = new Singleton(); 汇编代码: 0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: **lock** addl $0x0,(%esp);
这个lock前缀指令相当于上述的内存屏障,提供了以下保证:
- 将当前CPU缓存行的数据写回到主内存;
- 这个写回内存的操作会导致在其它CPU里缓存了该内存地址的数据无效。
-
总结
CPU为了提高处理性能,并不直接和内存进行通信,而是将内存的数据读取到内部缓存,再进行操作,但操作完并不能确定何时写回到内存,如果对volatile变量进行写操作,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,不过还是存在一个问题,就算内存的数据是最新的,其它CPU缓存的还是旧值,所以为了保证各个CPU的缓存一致性,每个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性,当发现自己缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会重新从内存中读取数据到缓存中。