volatile 关键字(修饰变量)
[TOC]
volatile 关键字(修饰变量)
1. 含义
是一种比 sychronized
关键字更轻量级的同步机制,访问 volatile
变量时,不会执行加锁操作。
注意:当一个属性变量声明成 volatile,就不能同时声明成 final 。
2. 作用
volatile 是一个类型修饰符(type specifier)。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
- 保证可见性
- 禁止指令重排序优化
指令重排序优化:普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序和程序代码中的执行顺序一致
3. 如何保证可见性
新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
-
volatile 修饰的变量,是直接拿的主内存的值,就是说这个值永远是最新的,对其他线程是可见的。
-
而访问非
volatile
变量时,每个线程都会从系统内存(主内存)拷贝变量到工作内存中,然后修改工作内存中的变量值,操控的变量可能不同。
4. 如何禁止指令重排序优化
volatile 通过设置 Java 内存屏障禁止重排序优化。
java 内存屏障
内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
java 的内存屏障通常所谓的四种即
LoadLoad
,StoreStore
,LoadStore
,StoreLoad
实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。(
Load
指令(也就是从内存读取),Store指令
(也就是写入内存)。)
LoadLoad 屏障:对于这样的语句
Load1; LoadLoad; Load2
,在 Load2 及后续读取操作要读取的数据被访问前,保证Load1 要读取的数据被读取完毕。StoreStore 屏障:对于这样的语句
Store1; StoreStore; Store2
,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。LoadStore 屏障:对于这样的语句
Load1; LoadStore; Store2
,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。StoreLoad 屏障:对于这样的语句
Store1; StoreLoad; Load2
,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile 做了什么
在一个变量被 volatile 修饰后,JVM 会为我们做两件事:
-
在每个 volatile 写操作前插入 StoreStore 屏障,在写操作后插入 StoreLoad 屏障。(
StoreStore-写-StoreLoad
) -
在每个 volatile 读操作前插入 LoadLoad 屏障,在读操作后插入LoadStore屏障。(
LoadLoad-读-LoadStore
)
5. volatile 是不安全的
虽然 volatile 可见性保证了对 volatile 变量所有的写操作都能立刻反应到其他线程之中(即 volatile 变量在各个线程中都是一致的),但是 Java 里面的运算并非原子操作。只有是原子操作的 volatile 变量才是线程安全的,比如我们很常见的 变量++ 自增操作,在这个过程中,自增包括取数,加一,保存三个过程的操作,所以自增并不是原子性操作,使用 volatile 修饰的变量自增操作仍然是不安全的。
举个例子:
public class MyVolitile {
private static volatile int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count++;
}
}
}).start();
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
结果每次都不一样,不一定等于 200。
6. volatile 不适用场景
由于 volatile 变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用 synchronized 或 java.util.concurrent 中的原子类)来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束