volatile特性java
内存可见性:通俗来讲就是,线程A对一个volatile变量的修改,对于其它线程来讲是可见的,即线程每次获取volatile变量的值都是最新的。缓存
volatile的使用场景多线程
经过关键字sychronize能够防止多个线程进入同一段代码,在某些特定场景中,volatile至关于一个轻量级的sychronize,并发
由于不会引发线程的上下文切换,可是使用volatile必须知足两个条件:
一、对变量的写操做不依赖当前值,如多线程下执行a++,是没法经过volatile保证结果准确性的;
二、该变量没有包含在具备其它变量的不变式中,这句话有点拗口,看代码比较直观。高并发
public class NumberRange { private volatile int lower = 0; private volatile int upper = 10; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }
上述代码中,上下界初始化分别为0和10,假设线程A和B在某一时刻同时执行了setLower(8)和setUpper(5),且都经过了不变式的检查,性能
设置了一个无效范围(8, 5),因此在这种场景下,须要经过sychronize保证方法setLower和setUpper在每一时刻只有一个线程可以执行。this
下面是咱们在项目中常常会用到volatile关键字的两个场景:spa
一、状态标记量
在高并发的场景中,经过一个boolean类型的变量isopen,控制代码是否走促销逻辑,该如何实现?线程
public class ServerHandler { private volatile isopen; public void run() { if (isopen) { //促销逻辑 } else { //正常逻辑 } } public void setIsopen(boolean isopen) { this.isopen = isopen } }
场景细节无需过度纠结,这里只是举个例子说明volatile的使用方法,用户的请求线程执行run方法,若是须要开启促销活动,3d
能够经过后台设置,具体实现能够发送一个请求,调用setIsopen方法并设置isopen为true,因为isopen是volatile修饰的,
因此一经修改,其余线程均可以拿到isopen的最新值,用户请求就能够执行促销逻辑了。
二、double check
单例模式的一种实现方式,但不少人会忽略volatile关键字,由于没有该关键字,程序也能够很好的运行,
只不过代码的稳定性总不是100%,说不定在将来的某个时刻,隐藏的bug就出来了。
class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
不过在众多单例模式的实现中,我比较推荐懒加载的优雅写法Initialization on Demand Holder(IODH)。
public class Singleton { static class SingletonHolder { static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; } }
固然,若是不须要懒加载的话,直接初始化的效果更好。
如何保证内存可见性?
在java虚拟机的内存模型中,有主内存和工做内存的概念,每一个线程对应一个工做内存,
并共享主内存的数据,下面看看操做普通变量和volatile变量有什么不一样:
一、对于普通变量:读操做会优先读取工做内存的数据,若是工做内存中不存在,则从主内存中拷贝一份数据到工做内存中;
写操做只会修改工做内存的副本数据,这种状况下,其它线程就没法读取变量的最新值。
二、对于volatile变量,读操做时JMM会把工做内存中对应的值设为无效,要求线程从主内存中读取数据;
写操做时JMM会把工做内存中对应的数据刷新到主内存中,这种状况下,其它线程就能够读取变量的最新值。
volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的,什么是内存屏障?内存屏障,又称内存栅栏,是一个CPU指令。
在程序运行时,为了提升执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不一样的编译器和CPU上有相同的结果,
经过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,
插入一条内存屏障会告诉编译器和CPU:无论什么指令都不能和这条Memory Barrier指令重排序。
这段文字显得有点苍白无力,不如来段简明的代码:
class Singleton { private volatile static Singleton instance; private int a; private int b; private int b; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { a = 1; // 1 b = 2; // 2 instance = new Singleton(); // 3 c = a + b; // 4 } } } return instance; } }
一、若是变量instance没有volatile修饰,语句一、二、3能够随意的进行重排序执行,即指令执行过程多是3214或1324。
二、若是是volatile修饰的变量instance,会在语句3的先后各插入一个内存屏障。
经过观察volatile变量和普通变量所生成的汇编代码能够发现,操做volatile变量会多出一个lock前缀指令:
Java代码:
instance = new Singleton();
汇编代码:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: **lock** addl $0x0,(%esp);
这个lock前缀指令至关于上述的内存屏障,提供了如下保证:
一、将当前CPU缓存行的数据写回到主内存;
二、这个写回内存的操做会致使在其它CPU里缓存了该内存地址的数据无效。
CPU为了提升处理性能,并不直接和内存进行通讯,而是将内存的数据读取到内部缓存(L1,L2)再进行操做,但操做完并不能肯定什么时候写回到内存,若是对volatile变量进行写操做,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,不过仍是存在一个问题,就算内存的数据是最新的,其它CPU缓存的仍是旧值,因此为了保证各个CPU的缓存一致性,每一个CPU经过嗅探在总线上传播的数据来检查本身缓存的数据有效性,当发现本身缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会从新从内存中读取数据到缓存中。
转载自:简书 占小狼 http://www.jianshu.com/p/195ae7c77afe