volatile关键字解析

来源:互联网 发布:js创建标签 编辑:程序博客网 时间:2024/06/05 20:37

1、volatile关键字修饰的变量(类的成员变量和类、类的静态成员变量)是不稳定的,但是能保证绝对的可见性。

 一旦一个变量被volatile关键字修饰之后,就具备了一下两层含义两层含义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说都是立即可见的。 2)禁止进行指令重排序。

看如下的代码段执行:

//线程1boolean stop = false;while(!stop){    doSomething();}//线程2stop = true;
 很多人试图用上述代码实现中断标记,但事实上代码并不一定能完全运行正确,大部分时候是可以实现线程中断的,但是也有可能无法导致线程中断,一旦这种情况出现,就有可能发生死循环。 下面解释一下为何这段代码有时候可能导致无法中断线程。这就要结合java内存模型的知识来讲解,我们知道每个线程在运行时候不会直接操作主内存中的变量,而是拷贝一份在自己的工作内存进行操作。所以,当线程2更改来了stop的值之后,还没来得及写入主存当中去,想成2转区做其他的事情,而由于线程1无法立即得知线程2对stop变量做了改动,因此线程1会一直循环下去。但是使用volatile修饰之后就变得不一样了: 第一:使用volatile关键字会强制将修改的值立即写入主存; 第二:使用volatile关键字的话,当线程2对变量进行修改时,会导致线程1的工作内存中对该变量的缓存无效; 第三:由于线程1的工作内存中缓存的变量无效,所以线程1再要读取变量的值时就要去主存当中去读取。

那么在线程2修改stop的值时(这儿包括两个操作:修改线程2工作内存的值,然后将修改后的值写入内存),会使得线程1的工作内存中stop的缓存变得无效,当线程1要读取stop的值时,发现缓存无效,那么他会等主存更新之后,然后去对应的主存读取最新的值。那么线程1读到的就是最新的正确的值。
2、volatile能保证原子性吗? 不能

    从上面一个例子知道,volatile能保证可见性,但是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。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

  可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=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对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

  那么两个线程分别进行了一次自增操作后,inc只增加了1。

  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

 以上问题通过synchronized关键字是可以解决的,因为volatile是一种弱化的同步机制,无法保证原子性,但是synchronized能够保证操作都hi顺序的,因此一般为了安全起见,都应该选择synchronized关键字。

3、volatile能保证有序性码? 前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性

volatile关键字指令冲排序有两层意思:1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

其实上面两句话的意思很明确:就是在程序执行过程中,被volatile修饰的变量是一道分水岭,在volatile变量前面的程序语句是可以局部重排序的,在volatile变量后面的程序语句也是可以重排序的,但是volatile所修饰的变量和其前后两部分程序之间的总体次序是不变的。
4、volatile的原理和实现机制:

 下面这段话摘自《深入理解Java虚拟机》: “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能: 1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; 2)它会强制将对缓存的修改操作立即写入主存; 3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

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

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

实际上这些条件表明,可以被写入volatile变量的这些有效值独立于任何程序的状态,包括变量的当前状态,事实上也就是只有在保证对volatile变量的操作是原子性的,才能够保证在使用volatile修饰的变量在程序运行中的正确性。
  

原创粉丝点击