并发程序想要正确执行,必须保证三个特性:
- 可见性
- 原子性
- 有序性
volatile关键字涉及了 内存模型 和 Java内存模型(JMM),需要大概了解这两部分内容,以及缓存一致性协议的内容
volatile 作用
volatile用于并发编程中,声明一个共享变量(成员变量 or 静态变量)的可见性,即:在缓存中变量被修改会被立即更新到主存中去
- 保证了不同线程对变量操作时的可见性,有一个线程修改了该变量值后,其他线程立即可见
- 禁止进行指令重排序
volatile主要是通过修改后立即更新到主存中,而其他线程的该共享变量会被标记为缓存行无效,会重新在主存中读取数据并写入到自己到缓存中去(缓存一致性协议),因此就保证了并发时数据的正确性
volatile不保证原子性
虽然 volatile 修饰的数据在修改后能马上更新到主存中去,保证了可见性,但是 volatile 并不能保证原子性;
在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性的,但是需要注意的是,如下:
x = 10; // 1y = x; // 2x++; // 3x = 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;}}
