你的位置:首页 > Java教程

[Java教程]Volatile关键字详解


  • 简介

  在java中,每个线程有一块工作内存区,其中存放这被所有线程共享的主内存中变量值的拷贝。当线程执行时,它在自己的工作内存中操作这些变量。为了获取一个共享变量,一个线程先获取锁定并清除它的工作内存区,这就保证了该共享变量从所有的线程的共享主内存区正确的装入到线程的工作内存区,当线程解锁时保证该工作内存区的变量的值写回到共享主内存区。

  线程工作内存和主内存的交互图如下:

  从上图中可以看出,主内存和线程工作内存间的数据传输与线程工作内存和线程执行有一定的时间间隔,而且每次所消耗的时间可能还不相同,这样就存在线程操作的数据的不一致性。由于每个线程都有自己的线程工作内存,因此当一个线程改变自己工作内存中的数据的时候,对于其他系统来说可能是不可见的。因此,使用volatile关键字迫使所有的线程均读写主内存中对应的变量,从而使得volatile关键字修饰的变量在多线程间可见。

  volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。

  声明为volatile的变量具有如下特性:

  1、其他线程对变量的修改可以即时反映在当前线程中。

  2、确保当前线程对volatile变量的修改,能即时的写回到共享主内存中,并被其他线程所见。

  3、使用volatile修饰的变量,编译器会保证其有序性。

  • volatile分析

  用在多线程,同步变量。 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。volatile就是用来避免这种情况的。volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的(也就是上面说的A) 

  下面一个测试例子:

 1 public class MyThread extends Thread{ 2   private volatile boolean stop = false;//确保stop在多线程中可见 3    4   public void stopMe(){ 5     stop = true; 6     System.out.println("stopMe"+System.currentTimeMillis()); 7   } 8  9   @Override10   public void run() {11     int i = 0;12     while(!stop){13       i++;14     }15     System.out.println("run"+System.currentTimeMillis());16     System.out.println("stop thread");17   }18   19 }

  如果stop没有被声明为volatile类型,那么线程在执行run的时候是检查自己的工作内存的副本,不能得知其他线程对stop的修改,因此线程无法结束。但是将stop被声明为volatile类型,那么在其他线程修改stop后,线程会立刻知道,则会跳出循环,正常结束。

  volitile与synchronized的区别

  Volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。如果配合Java 5增加的atomic wrapper classes,对它们的increase之类的操作就不需要sychronized。

  synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监视器能强制保证代码块同时只被一个线程所执行。volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。

  在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++ 等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。 volatile 变量不会像锁那样造成线程阻塞,在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。

  • 原理

  如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

 

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

 

  2)它会强制将对缓存的修改操作立即写入主存;

 

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

 

    把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。下面我们通过具体的示例来说明,请看下面的示例代码:

 1 class VolatileFeaturesExample { 2   volatile long vl = 0L; //使用volatile声明64位的long型变量 3   public void set(long l) { 4     vl = l;  //单个volatile变量的写 5   } 6   public void getAndIncrement () { 7     vl++;  //复合(多个)volatile变量的读/写 8   } 9   public long get() {10     return vl;  //单个volatile变量的读11   }12 }

  假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:

 1 class VolatileFeaturesExample { 2   long vl = 0L;        // 64位的long型普通变量 3   public synchronized void set(long l) {   //对单个的普通 变量的写用同一个监视器同步 4     vl = l; 5   } 6   public void getAndIncrement () { //普通方法调用 7     long temp = get();      //调用已同步的读方法 8     temp += 1L;         //普通写操作 9     set(temp);          //调用已同步的写方法10   }11   public synchronized long get() { 12   //对单个的普通变量的读用同一个监视器同步13     return vl;14   }15 }

  • Volatile与synchronized的区别:

  synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监视器能强制保证代码块同时只被一个线程所执行——这是众所周知的事实。但是,synchronized也同步内存:事实上,synchronized在“ 主”内存区域同步整个线程的内存。

  volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。

  Volatile只能保证可见性和有序性,对任意单个volatile变量的读/写具有原子性,所有在对volatile变量进行操作的时候要保证其操作是原子性,否则就要加锁来保证原子性。

  • Volatile的使用必须满足的条件

  1、对变量的写操作不依赖于当前值。

  2、该变量没有包含在具有其他变量的不变式中。