并发程序想要正确执行,必须保证三个特性:

  1. 可见性
  2. 原子性
  3. 有序性

volatile关键字涉及了 内存模型 和 Java内存模型(JMM),需要大概了解这两部分内容,以及缓存一致性协议的内容

volatile 作用

volatile用于并发编程中,声明一个共享变量(成员变量 or 静态变量)的可见性,即:在缓存中变量被修改会被立即更新到主存中去

  1. 保证了不同线程对变量操作时的可见性,有一个线程修改了该变量值后,其他线程立即可见
  2. 禁止进行指令重排序

volatile主要是通过修改后立即更新到主存中,而其他线程的该共享变量会被标记为缓存行无效,会重新在主存中读取数据并写入到自己到缓存中去(缓存一致性协议),因此就保证了并发时数据的正确性

volatile不保证原子性

虽然 volatile 修饰的数据在修改后能马上更新到主存中去,保证了可见性,但是 volatile 并不能保证原子性;

在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性的,但是需要注意的是,如下:

  1. x = 10; // 1
  2. y = x; // 2
  3. x++; // 3
  4. x = x + 1; // 4

其中,只有 1 是原子性的操作,而 2 中,包括对x值对读取,对y的赋值这两个操作,单独来看这两个操作都是原子性的,但是合并起来就不是原子性了,3 / 4 中类似,都是几个原子性的操作合并起来就不是原子性了,如果需要保证其原子性,只能通过 synchronized 和 lock 来实现

同时,就算使用volatile关键字修饰变量,也不能保证如上是原子性的,代码如下:

  1. public class Test {
  2. public volatile int inc = 0;
  3. public void increase() {
  4. inc++;
  5. }
  6. public static void main(String[] args) {
  7. final Test test = new Test();
  8. for(int i=0;i<10;i++){
  9. new Thread(){
  10. public void run() {
  11. for(int j=0;j<1000;j++)
  12. test.increase();
  13. };
  14. }.start();
  15. }
  16. while(Thread.activeCount()>1) //保证前面的线程都执行完
  17. Thread.yield();
  18. System.out.println(test.inc);
  19. }
  20. }

如果线程 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个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效;

volatile关键字的使用场景

synchronized 关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而 volatile关键字在某些情况下性能要优于 synchronized,但是要注意 volatile 关键字是无法替代synchronized 关键字的,因为 volatile 关键字无法保证操作的原子性。通常来说,使用 volatile 必须具备以下2个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

也就是说需要在原子性有保证的情况下使用 volatile 关键字,才能在并发时正确地执行操作;

列举的使用场景:

  1. 状态标记量
  1. volatile boolean flag = false;
  2. while(!flag){
  3. doSomething();
  4. }
  5. public void setFlag() {
  6. flag = true;
  7. }
  1. volatile boolean inited = false;
  2. //线程1:
  3. context = loadContext();
  4. inited = true;
  5. //线程2:
  6. while(!inited ){
  7. sleep()
  8. }
  9. doSomethingwithconfig(context);
  1. double check
  1. class Singleton{
  2. private volatile static Singleton instance = null;
  3. private Singleton() {
  4. }
  5. public static Singleton getInstance() {
  6. if(instance==null) {
  7. synchronized (Singleton.class) {
  8. if(instance==null)
  9. instance = new Singleton();
  10. }
  11. }
  12. return instance;
  13. }
  14. }

本文部分参考:http://www.cnblogs.com/dolphin0520/p/3920373.html