深刻剖析volatile关键字

volatile的原理和实现机制

若是一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:html

  1. 保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。
  2. 禁止进行指令重排序。

在Java中long赋值不是原子操做,由于先写32位,再写后32位,分两步操做,而AtomicLong赋值是原子操做,为何?为何volatile能替代简单的锁,却不能保证原子性?这里面涉及volatile,是java中的一个我以为这个词在Java规范中从未被解释清楚的神奇关键词,在Sun的JDK官方文档是这样形容volatile的:java

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.c++

意思就是说,若是一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对全部线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在全部CPU可见。volatile彷佛是有时候能够代替简单的锁,彷佛加了volatile关键字就省掉了锁。但又说volatile不能保证原子性(java程序员很熟悉这句话:volatile仅仅用来保证该变量对全部线程的可见性,但不保证原子性)。这不是互相矛盾吗?程序员

volatile到底如何保证可见性和禁止指令重排序的。“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”(摘自《深刻理解Java虚拟机》)lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:缓存

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;
  2. 它会强制将对缓存的修改操做当即写入主存;
  3. 若是是写操做,它会致使其余CPU中对应的缓存行无效。

例如你让一个volatile的integer自增(i++);安全

mov    0xc(%r10),%r8d ; Load(读取volatile变量值到local)
inc    %r8d           ; Increment(增长变量的值)
mov    %r8d,0xc(%r10) ; Store(把local的值写回,,让其它的线程可见)
lock addl $0x0,(%rsp) ; StoreLoad Barrier

什么是内存屏障(Memory Barrier)?

内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操做执行的顺序; b) 影响一些数据的可见性(多是某些指令执行后的结果)。编译器和CPU能够在保证输出结果同样的状况下对指令重排序,使性能获得优化。插入一个内存屏障,至关于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另外一个做用是强制更新一次不一样CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将获得最新值,而不用考虑究竟是被哪一个cpu核心或者哪颗CPU执行的。jvm

内存屏障(memory barrier)和volatile什么关系?上面的虚拟机指令里面有提到,若是你的字段是volatile,Java内存模型将在写操做后插入一个写屏障指令,在读操做前插入一个读屏障指令。这意味着若是你对一个volatile字段进行写操做,你必须知道:一、一旦你完成写入,任何访问这个字段的线程将会获得最新的值。二、在你写入前,会保证全部以前发生的事已经发生,而且任何更新过的数据值也是可见的,由于内存屏障会把以前的写入值都刷新到缓存。ide

volatile为何没有原子性?性能

明白了内存屏障(memory barrier)这个CPU指令,回到前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在全部线程可见,也就是最后一步让全部的CPU内核都得到了最新的值,但中间的几步(从Load到Store)是不安全的,中间若是其余的CPU修改了值将会丢失。下面的测试代码能够实际测试voaltile的自增没有原子性:测试

volatile可否保证可见性?(能够)

假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

  这段代码是很典型的一段代码,不少人在中断线程时可能都会采用这种标记办法。可是事实上,这段代码会必定会将线程中断么?不必定,也许在大多数时候,这个代码可以把线程中断,可是也有可能会致使没法中断线程(虽然这个可能性很小,可是只要一旦发生这种状况就会形成死循环了)。这是由于每一个线程在运行过程当中都有本身的工做内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本身的工做内存当中。那么当线程2更改了stop变量的值以后,可是还没来得及写入主存当中,线程2转去作其余事情了,那么线程1因为不知道线程2对stop变量的更改,所以还会一直循环下去。

  若是使用volatile修饰,第一,会强制将修改的值当即写入主存;第二,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);第三,因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。那么在线程2修改stop值时(固然这里包括2个操做,修改线程2工做内存中的值,而后将修改后的值写入内存),会使得线程1的工做内存中缓存变量stop的缓存行无效,而后线程1读取时,发现本身的缓存行无效,它会等待缓存行对应的主存地址被更新以后,而后去对应的主存读取最新的值。

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);
    }
}

  程序的运行结果是一个小于10000的数字。volatile的可见性只能保证每次读取的是最新的值,可是volatile没办法保证对变量的操做的原子性。在前面已经提到过,自增操做是不具有原子性的,它包括读取变量的原始值、进行加1操做、写入工做内存。那么就是说自增操做的三个子操做可能会分割开执行,就有可能致使下面这种状况出现:

  假如某个时刻变量inc的值为10,线程1对变量进行自增操做,线程1先读取了变量inc的原始值,而后线程1被阻塞了;而后线程2对变量进行自增操做,线程2也去读取变量inc的原始值,因为线程1只是对变量inc进行读取操做,而没有对变量进行修改操做,因此不会致使线程2的工做内存中缓存变量inc的缓存行无效,因此线程2会直接去主存读取inc的值,发现inc的值时10,而后进行加1操做,并把11写入工做内存,最后写入主存。而后线程1接着进行加1操做,因为已经读取了inc的值,注意此时在线程1的工做内存中inc的值仍然为10(由于线程1并无再次读取Iinc的值,而可见性只能保证每次读取的是最新的值),因此线程1对inc进行加1操做后inc的值为11,而后将11写入工做内存,最后写入主存。那么两个线程分别进行了一次自增操做后,inc只增长了1。

把上面的代码改为如下任何一种均可以达到效果:

采用synchronized:

public class Test {
    public  int inc = 0;
    public synchronized 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);
    }
}

采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    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);
    }
}

采用AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();
    public  void increase() {
        inc.getAndIncrement();
    }
    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);
    }
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做类,即对基本数据类型的 自增(加1操做),自减(减1操做)、以及加法操做(加一个数),减法操做(减一个数)进行了封装,保证这些操做是原子性操做。atomic是利用CAS来实现原子性操做的(Compare And Swap),CAS其实是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操做。

volatile可否保证有序性?(能够)

在前面提到volatile关键字能禁止指令重排序,因此volatile能在必定程度上保证有序性。volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可能上面说的比较绕,举个简单的例子:

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5

因为flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句一、语句2前面,也不会讲语句3放到语句四、语句5后面。可是要注意语句1和语句2的顺序、语句4和语句5的顺序是不做任何保证的。而且volatile关键字能保证,执行到语句3时,语句1和语句2一定是执行完毕了的,且语句1和语句2的执行结果对语句三、语句四、语句5是可见的。

那么咱们回到前面举的一个例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

前面举这个例子的时候,提到有可能语句2会在语句1以前执行,那么久可能致使context还没被初始化,而线程2中就使用未初始化的context去进行操做,致使程序出错。这里若是用volatile关键字对inited变量进行修饰,就不会出现这种问题了,由于当执行到语句2时,一定能保证context已经初始化完毕。

使用volatile关键字的场景

volatile关键字在某些状况下性能要优于synchronized,可是要volatile关键字是没法替代synchronized关键字的,由于volatile关键字没法保证操做的原子性。一般来讲,不要将volatile用在getAndOperate场合(这种场不是原子,须要再加锁),仅仅set或者get的场景是适合volatile的。

下面列举几个Java中使用volatile的几个场景。

1.状态标记量

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);

2.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;
    }
}

参考地址:http://www.cnblogs.com/dolphin0520/p/3920373.html

相关文章
相关标签/搜索