首先看看如下代码以及结果:(计算一秒钟内count++的结果)
public class VolatileDemo { private static Boolean flag = true; //创建一个状态变量flag public static void main(String[] args) { Thread thread = new Thread(()-> { //创建一个线程 int count = 0; while (flag){ count++; } System.out.println("count:"+count); }); Thread thread1 = new Thread(()->{//创建一个线程 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = false; System.out.println("1秒结束"); }); thread1.start(); thread.start(); } }
由运行结果可见,程序一直未停下来,说明变量flag修改无法实时同步给另一个线程。
当加入volatile关键字修饰状态变量flag:
public class VolatileDemo { private volatile static Boolean flag = true; public static void main(String[] args) { Thread thread = new Thread(()-> { int count = 0; while (flag){ count++; } System.out.println("count:"+count); }); Thread thread1 = new Thread(()->{ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = false; System.out.println("1秒结束"); }); thread1.start(); thread.start(); } }
为什么会这样?
在不加volatile关键时,线程堆栈中保存线程运行时的变量值的副本,当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。
volatile的特征
- 禁止重排序 代码在执行过程中,为了提高代码的执行效率,会对代码做优化,编译、字节码,机器码、汇编都会对代码进行优化,代码优化的原则是优化前后是不会来影响执行结果;Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序,永远是按照其出现顺序执行的。重排序的依据是happens-before法则。
- 保证内存的可见性
工作原理:
在volatile关键字所修饰的变量时,在汇编层代码上,会添加一个lock前缀的指令,Lock前缀指令相当于添加了一个内存屏障,内存屏障提供的功能:
1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2、它会强制将对缓存的修改操作立即写入主存;
3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
禁止指令重排序的原理
通过内存屏障实现,StoreStore\StoreLoad\LoadLoad\LoadStore屏障,保证写与写、写与读、读与读、读与写等有序。
可见性原理
解决内存一致性问题:JVM向处理器发送一个Lock前缀的指令,两种解决方案:
第一种:通过总线加LocK锁前缀的方法锁定总线。
第二种:通过缓存一致性协议(MESI协议)(缓存行 维护两个状态位 M,E,S,I)。
M(被修改的):处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
E(独占的):处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改, 即与内存中一致。
S(共享的):处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
I(无效的):本CPU中的这份缓存已经无效。
缓存一致性协议执行过程:
一个处于E (独占的)状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S;一个处于S (共享的)状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。一个处于M (被修改的)状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取(无效的就要读取新值),并把自己状态变成S (既然是无效那么说明已有cpu对缓存做了修改),如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
普通变量:
1,变量值从主内存(在堆中)load到本地内存(在当前线程的栈桢中);
2,之后,线程就不再和该变量在主内存中的值由任何关系,而是直接操作在副本变量上(这样速度很快),这时,如果主存中的count或本地内存中的副本发生任何变化,都不会影响到对方,也正是这个原因导致并发情况下出现数据不一致;
3,在修改完的某个时刻(线程退出之前),自动把本地内存中的变量副本值回写到对象在堆中的对应变量。
volatile修饰的变量:
volatile仍然在执行一个从主存加载到工作内存,并且将变更的值写回主存的操作,但是:
1,volatile保证每次读取该变量前,都判断当前值是否已经失效(即是否已经与主存不一致),如果已经失效,则从主存load最新的变量;
2,volatile保证每次对该变量做出修改时,都立即写入主存。
注意:volatile保证共享数据的可见性,有序性,却无法保证数据的原子性。
应用场景
- 一般用来修饰Boolean类型的共享状态标志位。
- 单例模式下的双重校验锁。
- 修饰单个的变量。
注意:volatile对于基本数据类型(值直接从主内存向工作内存copy)才有用。但是对于对象来说,似乎没有用,因为volatile只是保证对象引用的可见性,而对对象内部的字段,它保证不了任何事。即便是在使用ThreadLocal时,每个线程都有一份变量副本,这些副本本身也是存储在堆中的,线程栈桢中保存的仍然是基本数据类型和变量副本的引用。一个对象被volatile修饰,那么就表示它的引用具有了可见性。从而使得对于变量引用的任何变更,都在线程间可见,但是却不具备原子性。