Volatile的详解

volatile关键字修饰的共享变量主要有两个特色:1.保证了不一样线程访问的内存可见性    2.禁止重排序java

在说内存可见性和有序性以前,咱们有必要看一下Java的内存模型(注意和JVM内存模型的区分)c++

为何要有java内存模型?缓存

首先咱们知道内存访问和CPU指令在执行速度上相差很是大,彻底不是一个数量级,为了使得java在各个平台上运行的差距减小,哪些搞处理器的大佬就在CPU上加了各类高速缓存,来减小内存操做和CPU指令的执行速度差距。而Java在java层面又进行了一波抽象,java内存模型将内存分为工做内存和主存,每一个线程从主存load数据到工做内存,将load的数据赋值给工做内存上的变量,而后该工做内存对应的线程进行处理,处理结果在赋值给其工做内存,而后再将数据赋值给主存中的变量(这时候须要有一张图)。多线程

使用工做内存和主存虽然加快了处理速度,可是也带来了一些问题,好比下面这个例子并发

1         int i = 1;
2         i = i+1;

当在单线程状况下,i最后的值必定是2;可是在两个线程状况下必定是3吗?那就未必了。当线程A读取i的值为1,load到其工做内存,这时CPU切换至线程B,线程B读取i的值也是1,而后对加1而后save到主存,这时线程A也对i进行加1,也save回主存,但最终i的值为2。若是写操做比较慢,你读到的值还有多是1,这就是缓存不一致的问题。JMM就是围绕着原子性,内存可见性,有序性这三个特征创建的。经过解决这个三个特征来解决缓存不一致的问题。而volatile主要针对于内存可见性和有序性。app

原子性性能

原子性是指一个操做要么成功,那么失败,没有中间状态,好比i=1,直接读取i的值,这确定是原子操做;可是i++,看似好像是,其实须要先读取i的值,而后+1,最后在赋值给i,须要三个步骤,这就不是原子性操做。在JDK1.5引入了boolean、long、int对应的原子性类AtomicBoolean、AtomicLong、AtomicInteger,他们能够提供原子性操做。atom

内存可见性spa

具备内存可见性的变量在被线程修改之后,会马上刷新到主存并使其余线程的缓存行上的数据失效线程

volatile修饰的变量具备内存可见性,主要表现为:当写一个volatile变量时,JMM会将该线程对应的工做内存中的共享变量当即刷新到主存;当读一个volatile变量时,JMM会把该线程对应的工做内存中的值置为无效,而后从主存中进行读取,可是若是没有线程对该共享变量进行修改,则不会触发该操做。

有序性

JMM是容许处理器和编译器对指令进行重排序的,但规定了as-if-serial,即不管怎么重排序,最终结果都是同样的。好比下面这段代码:

1         int weight = 10;                           //A
2         int high = 5;                                //B
3         int area = high * weight * high;    //C

这段代码中能够按照A-->B-->C执行,也能够按照B-->A-->C执行,由于A和B是相互独立的,而C依赖于A、B,因此C不能排到A或B的前面。JMM保证了单线程的重排序,可是在多线程中就容易出现问题。好比下面这种状况

 1 boolean flag = false;
 2     int a = 0;
 3     
 4     public void write(){
 5         int a = 2;                //1
 6         flag = true;              //2
 7     }
 8     public void multiply(){
 9         if(flag){                //3
10             int ret = a * a ;    //4
11         }
12     }

若是有两个线程执行上面的代码,线程1先执行write方法,随后线程2执行multiply方法。最后结果必定是4吗,不必定。

如图,JMM对1和2进行了重排序,先将flag设置为true,这是线程2执行,因为a尚未赋值,因此最后ret的值为0;

若是使用volatile关键字修饰flag,禁止重排序,能够保证程序的有序性,也可使用synchronized或者lock这种重量级锁来保证有序性,但性能会降低。

另外,JMM具有一些先天的有序性,即不须要经过任何手段就能够保证的有序性,一般称为happens-before原则。<<JSR-133:Java Memory Model and Thread Specification>>定义了以下happens-before规则:

  1. 程序顺序规则: 一个线程中的每一个操做,happens-before于该线程中的任意后续操做

  2. 监视器锁规则:对一个线程的解锁,happens-before于随后对这个线程的加锁

  3. volatile变量规则: 对一个volatile域的写,happens-before于后续对这个volatile域的读

  4. 传递性:若是A happens-before B ,且 B happens-before C, 那么 A happens-before C

  5. start()规则: 若是线程A执行操做ThreadB_start()(启动线程B) , 那么A线程的ThreadB_start()happens-before 于B中的任意操做

  6. join()原则: 若是A执行ThreadB.join()而且成功返回,那么线程B中的任意操做happens-before于线程A从ThreadB.join()操做成功返回。

  7. interrupt()原则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,能够经过Thread.interrupted()方法检测是否有中断发生

  8. finalize()原则:一个对象的初始化完成先行发生于它的finalize()方法的开始

第1条规则程序顺序规则是说在一个线程里,全部的操做都是按顺序的,可是在JMM里其实只要执行结果同样,是容许重排序的,这边的happens-before强调的重点也是单线程执行结果的正确性,可是没法保证多线程也是如此。

第2条规则监视器规则其实也好理解,就是在加锁以前,肯定这个锁以前已经被释放了,才能继续加锁。

第3条规则,就适用到所讨论的volatile,若是一个线程先去写一个变量,另一个线程再去读,那么写入操做必定在读操做以前。

第4条规则,就是happens-before的传递性。

 

须要注意的是,被volatile修饰的共享变量只知足内存可见性和禁止重排序,并不能保证原子性。好比volatile i++。

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

按道理来讲结果是10000,可是运行下极可能是个小于10000的值。有人可能会说volatile不是保证了可见性啊,一个线程对inc的修改,另一个线程应该马上看到啊!但是这里的操做inc++是个复合操做啊,包括读取inc的值,对其自增,而后再写回主存。

假设线程A,读取了inc的值为10,这时候被阻塞了,由于没有对变量进行修改,触发不了volatile规则。

线程B此时也读读inc的值,主存里inc的值依旧为10,作自增,而后马上就被写回主存了,为11。

此时又轮到线程A执行,因为工做内存里保存的是10,因此继续作自增,再写回主存,11又被写了一遍。因此虽然两个线程执行了两次increase(),结果却只加了一次。

有人说,volatile不是会使缓存行无效的吗?可是这里线程A读取到线程B也进行操做以前,并无修改inc值,因此线程B读取的时候,仍是读的10。

又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?可是线程A的读取操做已经作过了啊,只有在作读取操做时,发现本身缓存行无效,才会去读主存的值,因此这里线程A只能继续作自增了。

综上所述,在这种复合操做的情景下,原子性的功能是维持不了了。可是volatile在上面那种设置flag值的例子里,因为对flag的读/写操做都是单步的,因此仍是能保证原子性的。

要想保证原子性,只能借助于synchronized,Lock以及并发包下的atomic的原子操做类了,即对基本数据类型的 自增(加1操做),自减(减1操做)、以及加法操做(加一个数),减法操做(减一个数)进行了封装,保证这些操做是原子性操做。

 

volatile底层原理

若是将使用volatile修饰的代码和未使用volatile修饰的代码都编译成汇编语言,会发现,使用volatile修饰的代码会多出一个lock前缀指令。

lock前缀指令至关于一个内存屏障,内存屏障的做用有如下三点:

①重排序时,不能把内存屏障后面的指令排序到内存屏障前

②使得本CPU的cache写入内存

③写入动做会引发其余CPU缓存或内核的数据无效,至关于修改对其余线程可见。

 

volatile的应用场景

由于volatile对复合操做无效,因此volatile修饰像上面例子中的flag这样的只会发生读/写的标记型字段。

在单利模式中,volatile还能够修饰成员变量,防止初始化时的指令重排序。

 1 class Singleton{
 2     private volatile static Singleton instance= null;
 3     
 4     private Singleton(){
 5         
 6     }
 7     
 8     public static Singleton getInstance(){
 9         if(instance==null){
10             synchronized(Singleton.class){
11                 if(instance==null){
12                     instance = new Singleton();
13                 }
14             }
15         }
16         return instance;
17     }
18 }
相关文章
相关标签/搜索