英 /ˈvɒlətaɪl/ 我了太噢(记不住单词怎么读)
volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性。相比于synchronized关键字,volatile关键字的执行成本更低,效率更高。
可见性和禁止指令重排
。 volatile 提供 happens-before的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
并发编程的三大特性为可见性、有序性和原子性。通常来讲volatile可以保证可见性和有序性。
能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
volatile的实现原理也是围绕如何实现可见性和有序性
展开的。
导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不一致所导致,例如上面所说线程A访问自己本地内存A的X值时,但此时主内存的X值已经被线程B所修改,所以线程A所访问到的值是一个脏数据。那如何解决这种问题呢?
volatile可以保证内存可见性的关键是volatile的读/写实现了缓存一致性,缓存一致性的主要内容为:
MESI(缓存一致性协议):当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。
那缓存一致性是如何实现的呢?可以发现通过volatile修饰的变量,生成汇编指令时会比普通的变量多出一个Lock指令,这个Lock指令就是volatile关键字可以保证内存可见性的关键,它主要有两个作用:
为了实现volatile的内存语义,编译器在生成字节码时会通过插入内存屏障
来禁止指令重排序。
内存屏障
:内存屏障是一种CPU指令,它的作用是对该指令前和指令后的一些操作产生一定的约束,保证一些操作按顺序执行。
Java虚拟机插入内存屏障的策略
Java内存模型把内存屏障分为4类,如下表所示:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 保证Load1数据的读取先于Load2及后续所有读取指令的执行 |
StoreStore Barriers | Store1;StoreStore;Store2 | 保证Store1数据刷新到主内存先于Store2及后续所有存储指令 |
LoadStore Barriers | Load1;LoadStore;Store2 | 保证Load1数据的读取先于Store2及后续的所有存储指令刷新到主内存 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 保证Store1数据刷新到主内存先于Load2及后续所有读取指令的执行 |
注:StoreLoad Barriers同时具备其他三个屏障的作用,它会使得该屏障之前的所有内存访问指令完成之后,才会执行该屏障之后的内存访问命令。
Java内存模型对编译器指定的volatile重排序规则为:
根据volatile重排序规则,Java内存模型采取的是保守的屏障插入策略
,volatile写是在前面和后面分别插入
内存屏障,volatile读是在后面插入
两个内存屏障,具体如下:
StoreStore屏障的作用:禁止上面的普通写和下面的volatile写重排序
StoreLoad屏障的作用:防止上面的volatile写与下面可能出现的volatile读/写重排序。
volatile
只能保证可见性和有序性,但可以保证64位
的long型和double型变量的原子性。
对于32位的虚拟机来说,每次原子读写都是32位的,会将long和double型变量拆分成两个32位的操作来执行,这样long和double型变量的读写就不能保证原子性了。
因此,通过volatile修饰的long和double型变量则可以保证其原子性。
所以从Oracle Java Spec里面可以看到:
单例模式
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:对于Double-Check这种可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile关键字即可volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。注意:volatile阻止的不是singleton = newSingleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。
public class Singleton7 {
private static volatile Singleton7 instance = null;
private Singleton7() {}
public static Singleton7 getInstance() {
if (instance == null) {
synchronized (Singleton7.class) {
if (instance == null) {
instance = new Singleton7();
}
}
}
return instance;
}
}
volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
volatile
表示:变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
synchronized
表示:只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 除了用于保障 long/ double 型变量的读、写操作的原子性,其典型使用场景还包括以下几个方面: