并发程序想要正确执行,必须保证三个特性:
- 可见性
- 原子性
- 有序性
volatile关键字涉及了 内存模型 和 Java内存模型(JMM),需要大概了解这两部分内容,以及缓存一致性协议的内容
volatile 作用
volatile用于并发编程中,声明一个共享变量(成员变量 or 静态变量)的可见性,即:在缓存中变量被修改会被立即更新到主存中去
- 保证了不同线程对变量操作时的可见性,有一个线程修改了该变量值后,其他线程立即可见
- 禁止进行指令重排序
volatile主要是通过修改后立即更新到主存中,而其他线程的该共享变量会被标记为缓存行无效,会重新在主存中读取数据并写入到自己到缓存中去(缓存一致性协议),因此就保证了并发时数据的正确性
volatile不保证原子性
虽然 volatile 修饰的数据在修改后能马上更新到主存中去,保证了可见性,但是 volatile 并不能保证原子性;
在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性的,但是需要注意的是,如下:
x = 10; // 1
y = x; // 2
x++; // 3
x = x + 1; // 4
其中,只有 1 是原子性的操作,而 2 中,包括对x值对读取,对y的赋值这两个操作,单独来看这两个操作都是原子性的,但是合并起来就不是原子性了,3 / 4 中类似,都是几个原子性的操作合并起来就不是原子性了,如果需要保证其原子性,只能通过 synchronized 和 lock 来实现
同时,就算使用volatile关键字修饰变量,也不能保证如上是原子性的,代码如下:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
如果线程 1 从主存中读取到 inc 到值 → 对 inc 进行++ 操作后被阻塞,此时,主存中的 inc 值还是为0,线程二,读取主存中inc的值,并执行++ 操作后重新写入主存中(inc = 1),此时,线程 1 缓存中的 inc 行被标记为无效缓存,cpu 轮转到线程 1 执行,线程 1 将 inc = 1 写入主存,就导致了主存中的 inc 被加了两次,实际上结果只加了一次,导致最后的结果不正确(最后该程序的输出结果一定小于10000)
volatile 一定程度保证有序性
在 Java 中规定 volatile 修饰的变量禁止进行指令重排序,所以 volatile 能在一定程度上保证有序性
volatile的原理和实现机制
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效;
volatile关键字的使用场景
synchronized 关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而 volatile关键字在某些情况下性能要优于 synchronized,但是要注意 volatile 关键字是无法替代synchronized 关键字的,因为 volatile 关键字无法保证操作的原子性。通常来说,使用 volatile 必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
也就是说需要在原子性有保证的情况下使用 volatile 关键字,才能在并发时正确地执行操作;
列举的使用场景:
- 状态标记量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
- double check
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;
}
}