目录html
Java语言规范第三版中对volatile的定义以下:java编程语言容许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保经过排他锁单独得到这个变量。Java语言提供了volatile,在某些状况下比锁更加方便。若是一个字段被声明成volatile,java线程内存模型确保全部线程看到这个变量的值是一致的。
volatile变量修饰符若是使用恰当的话,它比synchronized的使用和执行成本会更低,由于它不会引发线程上下文的切换和调度。java
处理器为了提升处理速度,不直接和内存进行通信,而是先将系统内存的数据读到内部缓存L1,L2或其余)后再进行操做,但操做完以后不知道什么时候会写到内存,若是对声明了Volatile变量进行写操做,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。可是就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题,因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里。c++
LOCK前缀指令会引发缓存回写到内存,LOCK前缀指令致使执行指令期间,声明处理器的LOCK#信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器能够独占使用任何共享内存。(由于它会锁住总线,致使其余CPU不能访问总线,不能访问总线就意味着不能访问系统内存)程序员
LOCK#信号通常不锁总线,而是锁缓存,毕竟锁总线开销比较大。对于Intel486和Pentium处理器,在锁操做时,老是在总线上声言LOCK#信号。但在P6和最近的处理器中,若是访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操做被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据 。
一个处理器的缓存回写到内存会致使其余处理器的缓存无效 。IA-32处理器和Intel 64处理器使用MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其余处理器缓存的一致性。在多核处理器系统中进行操做的时候,IA-32 和Intel 64处理器能嗅探其余处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其余处理器的缓存的数据在总线上保持一致。例如在Pentium和P6 family处理器中,若是经过嗅探一个处理器来检测其余处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次访问相同内存地址时,强制执行缓存行填充。编程
public class VolatileTest { //可见性 private /**volatile**/ static int INIT_VALUE = 0; private final static int MAX_VALUE = 5; public static void main(String[] args) throws InterruptedException { new Thread(() -> { int localVale = INIT_VALUE; while (localVale < MAX_VALUE) { /*** 对INIT_VALUE没有volatile关键字修 ***/ //为何这里一直没有去从主内存中拿数据进行刷新呢? //这是由于java认为这里没有writer的操做,因此不须要去主内存中获取新的数据。这个具体最新的值被刷新指 //具体能够VolatileTest2进行比较 //在这里加一个sysytem的输出是有可能会去刷新主存的,或者每次运行的时候休眠一小段时间, // 这个程序是有可能会结束的。若是没有System的输出,或者休眠,在while判断会一直不去主内存 //刷新新数据,也就致使程序一直无法结束。 //System.out.println("="); /*try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }*/ if (localVale != INIT_VALUE) { System.out.println("The value updated to [ " + INIT_VALUE + " ]"); localVale = INIT_VALUE; } } },"READER").start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { int localValue = INIT_VALUE; while (INIT_VALUE < MAX_VALUE) { System.out.println("update the value to [ " + (++localValue) + " ]"); INIT_VALUE = localValue; try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"UPDATER").start(); } }
Java的内存模型具有一些天生的有序规则,不须要任何同步手段就可以保证的有序性,这个规则被称为Happens-before原则,若是两个操做的执行顺序没法从happens-before原则推导出来,那么它们就没法保证有序性,也就是说虚拟机或处理器能够随意对它们进行重排序处理。缓存
由程序次序规则,在单线程的状况下,对于指令重排序不会出现什么问题,可是对于多线程的状况下,就颇有可能会因为指令重排序出现问题。
volatile关键字直接禁止JVM和处理器对volatile关键字修饰的指令重排序,可是对于volatile先后五以来的指令则能够随便怎么排序多线程
int x = 10 int y = 20 /** 在语句volatile int z = 20以前,先执行x的定义仍是先执行y的定义,咱们并不关心,只要可以百分百 保证在执行到z=20的时候,x=0,y=1已经定义好,同理对于x的自增以及y的自增操做都必须在z=20之后才能发生,这个规则能够认为是由程序次序规则+volatile规则推导 **/ valatile int z = 20 x++; y++;
private volatile boole init = false; private Context context ; public Context context() { if(!init){ context = loadContext(); /** 若是init不使用volatile关键字修饰的话,因为编译器会对指令作必定的优化,也就是指令重排序。 因此在由多线程执行的状况下,如某个线程A它可能执行init = true,后执行context = loadContext(), 由于这两条指令并无任何的依赖关系,因此执行顺序可能不定。当线程B执行到判断的时候,发现init=true成立, 那么线程B就不会再去加载context啦,此时若是它使用context,有可能context在线程A中尚未加载成功,此时线程B去 使用context就有可能报空指针异常。 而volatile关键字能阻止指令重排序,也就是说在init=true以前必定保证context=loadContext()执行完毕。 **/ init = true; //阻止指令重排序 } }
其实被volatile修饰的变量存在一个“lock”的前缀。
lock前缀实际上至关因而一个内存屏障,该内存屏障会为指令的执行提供以下几个保障
1.确保指令重排序不会将其后面的代码排到内存屏障以前
2.确保指令重排序不会将其前面的代码拍到内存屏障以后。
3.确保在执行内存屏障修饰的指令时前面的代码所有执行完成(1,2,3阻止了指令重排序)
4.强制将线程工做内存中的修改刷新至主内存中
5.若是是写操做,则会致使其余线程的工做内存(CPU Cache)中的缓存数据失效。(4,5保证了内存可见性)并发
volatile没法保证原子性
原子性:一个操做或多个操做,要么都成功,要么都失败,中间不能因为任何的因素中断
对基本数据类型的变量读取和赋值是保证了原子性的,要么都成功,要么都失败,这些操做不可被中断
a = 10; 原子性
b = a; 不知足1.read a; 2.assign to b;
c++; 不知足1.read c; 2.add 3.assign to c
c = c + 1; 不知足1.read c; 2.add 3.assign to capp
public class VolatileTest2 { //虽然保证了可见性,可是没有保证原子性 private volatile static int INIT_VALUE = 0; private final static int MAX_VALUE = 50; public static void main(String[] args) { new Thread(() -> { while (INIT_VALUE < MAX_VALUE) { //颇有可能会输出重复的数字 System.out.println("ADD-1-> " + (++INIT_VALUE)); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD-1").start(); new Thread(() -> { while (INIT_VALUE < MAX_VALUE) { System.out.println("ADD-2-> " + (++INIT_VALUE)); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD-2").start(); } }
(1) 使用上的区别jvm
并发之volatile底层原理:http://www.javashuo.com/article/p-fgsreuii-a.html