赞
踩
volatile关键字是Java虚拟机提供的最轻量级的同步机制,作为一个修饰符出现,用来修饰变量,但不包括局部变量。
先来一段demo代码:
public class Test { public static void main(String[] args) { Aobing a =new Aobing(); a.start(); for(;;){ if(a.isFlag()){ System.out.println("冲冲冲"); } } } } class Aobing extends Thread{ private boolean flag =false; public boolean isFlag(){ return flag; } @Override public void run(){ try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } flag =true; System.out.println("flag="+flag); } }
在主程序中,有一个死循环来监控flag的状态,当状态变为true时,输出冲冲冲,按理说只要子线程中打印出flag的状态来就表明flag变为true,冲冲冲就会被打印出来。
但是在实例运行过程中是不会输出冲冲冲的。那又是为什么呢。
原因是flag虽然发送了变化,但是由于flag的不可见,导致第一个线程不知道flag发生了变化所以一直未输出。针对这个问题可以在变量上加上volatile,来让这个变量具有可见性,从而让第一个线程来跳出循环。
由上面的例子我们可以得知Volatile具有让变量对所有线程可见性的能力,除此之外还有如下的能力。
为了更好的理解Volatile,我们有必要先来看一下计算机内存模型以及JMM。
由于现在存储设备与处理器的运算速度差距太大,所以需要加入高速缓存(cache)来作为内存与处理器之间的缓冲。这样处理器无需等待缓慢的内存读写了
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享一个主内存
虽然高速缓存很好的解决了处理器与内存的速度矛盾,但是也引入了一个新的问题:缓存一致性。
对此有两个方案来改变:
CPU和其他功能组件是通过总线通信的,如果在总线加LOCK锁,那么在锁住总线期间,其他CPU是无法访问内存的。虽然能解决缓存一致性问题,但是会导致效率低下
为了解决一致性问题,还可以通过缓存一致性协议,即各个处理器访问缓存时都遵循统一的协议,在读写时要根据协议来进行操作。其核心思想就是:
当CPU写数据时,如果发现操作的变量是共享变量,就会发出通知其他CPU将该缓存行置位无效状态。
CPU每个缓存行标记的四种状态(M、E、S、I)
缓存状态 描述 M(被修改) 该缓存行只被该CPU缓存,与主存的值不同,会在它被其他CPU读取之前写入内存,并设置为Shared E(独享的) 该缓存行只被该CPU缓存,与主存的值相同,被其他CPU读取时置为Shared,被其他CPU写时置为Modified S(共享的) 该缓存行可能被多个CPU缓存,各个缓存中的数据与主存数据相同 I(无效的) 该缓存行数据是无效的,需要时需要从主存载入
MESI协议的实现以及保证当前处理器的内部缓存、主内存和其他处理器的缓存数据在总线上的一致性需要依靠多处理器总线嗅探
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是否过期。如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据库读到处理器缓存中。
这里的变量指的时候实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题
线程对变量的所有的操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量
由于这种机制才导致可见性问题的存在,下面就讨论有关可见性的解决方案。
指操作是不可中断的,要么执行完成,要么不执行,基本数据类型的访问和读写都是具有原子性的。
如果在本线程内观察,所有的操作都是有序的;如果在一个线程内观察另外一个线程,所有的操作都是无序的
即在java内存模型中,荀彧编译器和处理器对执行进行重排序,会影响多线程并发执行的正确性,但不管怎么重排序,单线程的执行结果不会改变
重排序:编译器和处理器为了提高并行度。
CPU重排序包括指令并行重排序和内存系统重排序
- 过程:
通过上面我们得知Volatile的语义就是保证变量对所有线程可见性以及禁止指令重排优化。那么是如何保证的呢?这都与内存屏障有关
Volatile能保证修饰的flag变量后,可以立即同步回主内存中。并且每次在使用前立即先从主内存刷新最新的值。
java编译器会在生成指令系列时在适当的位置会插入内存屏障来禁止特定类型的处理器重排序
需要注意的是:volatile写 是在前面和后面分别插入内存屏障,而volatile读 是在后面插入两个内存屏障
内存屏障保证了:
通常来说,使用Volatile必须具备以下两个条件
实际上Volatile场景一般就是状态标志以及DCL单例模式
Map configOptions; char[] configText; // 此变量必须定义为 volatile volatile boolean initialized = false; // 假设以下代码在线程 A 中运行 // 模拟读取配置信息, 当读取完成后将 initialized 设置为 true 以告知其他线程配置可用 configOptions = new HashMap(); configText = readConfigFile(fileName); processConfigOptions(configText, configOptions); initialized = true; // 假设以下代码在线程 B 中运行 // 等待 initialized 为 true, 代表线程 A 已经把配置信息初始化完成 while(!initialized) { sleep(); } // 使用线程 A 中初始化好的配置信息 doSomethingWithConfig();
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
也是内存屏障哦,跟面试官讲下Java内存的保守策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
再讲下volatile的语义哦,重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置
不可以,原子性需要synchronzied或者lock保证
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。