6、volatile原理与使用

摘要:Java的volatile关键字对效率的影响Java关键字用于将一个变量标记为“存储在内存中的变量”。更准确的说,意思就是每一次对volatile标记的变量进行读取的时候,都是直接从电脑的主内存进行的,而不是从cpu的cache中,而且每个对volatile变量的写入操作,都会被直接写入到主存里,而不是只写

  • Java关键字用于将一个变量标记为“存储在内存中的变量”。更准确的说,意思就是每一次对volatile标记的变量进行读取的时候,都是直接从电脑的主内存进行的,而不是从cpu的cache中,而且每个对volatile变量的写入操作,都会被直接写入到主存里,而不是只写到cache里。


    实际上,从java5开始,volatile关键字就不仅仅是保证volatile变量从主存读写

    Java的volatile关键字可以保证变量的可见性:

    在多线程的应用程序中,线程操作非volatile的变量,为了更快速的执行程序,每个线程都会将变量从主存复制到cpu的cache中。如果你的电脑有多个cpu,每个线程都在不同的cpu上运行,这就意味着,每个线程将变量的值复制到不同的cpu的cache上,就像下面这个图所表明:


    深入理解Java多线程中的volatile关键字 

    如果变量没有声明为volatile,那么就无法知道,变量什么时候从主存中读取到cpu的cache中,有什么时候从cache中写回到主存中。这就可能造成很多潜在的问题:

    假设一种情况,多个线程同时持有一个共享对象的引用,这个对象包括一个counter变量:

    public class SharedObject { 
       public int counter = 0; 
    } 

    假设这种情况,只有线程1自增了这个counter变量,但是线程1和线程2可能随时读取这个counter变量。如果这个counter变量没有被声明为volatile,那么就无法确认,什么时候counter的变量的值会从cpu的cache中写回到主存中,这就意味着,counter变量的值在cpu的cache中的值可能和主存中不一样,如下图所示:


    深入理解Java多线程中的volatile关键字 

    这个线程的问题无法及时的看到变量的最新的值,因为可能这个变量还没有被另一个线程写回到主存中。所以一个线程对一个变量的更新对其他的线程是不可见的。这就是我们最初提出的线程的可见性问题。


    通过将一个变量声明为volatile,那么所有对这个变量写操作会被直接写回到主内存中,所以这对线程都是可见的。而且,所有对这个变量的读取操作,也会直接从主存中读取,下面说明了如何声明一个voaltile变量:

    public class SharedObject { 
     public volatile int counter = 0; 
    } 

    将一个变量声明为volatile就可以保证写操作,其他线程对这个变量的可见性


    从java5开始,volatile关键字不仅可以保证变量直接从主内存中读取,还有以下作用:


    如果线程A对一个volatile变量进行写操作,线程B随后读取同一个volatile值,那么在线程将变量写操作完成之后的所有变量对线程A和B都是可见的。 
    那些操作volatile变量的读写指令的顺序无法被JVM改变(JVM有时候为了效率会改变变量读写顺序,只要JVM判断改变顺序对程序没有影响的话)。 
  • 上面两段话不是很理解,我们接下来进行一个更细致的说明:

    当一个线程对一个volatile变量进行写操作的时候,不仅仅是这个变量自己被写入到主存中,同时,其他所有在这之前被改变值的变量也都会线程先写入到主存中。
    当一个线程对一个volatile变量进行读取操作,他也会将所有跟着那个volatile变量一起写入到主存中的其他所有变量一起读出来。
    看下面这个例子:

    Thread A: 
    sharedObject.nonVolatile = 123; 
    sharedObject.counter = sharedObject.counter + 1; 
    Thread B: 
    int counter = sharedObject.counter; 
    int nonVolatile = sharedObject.nonVolatile; 

    因为线程A在对volatile的sharedObject.counter进行写操作之前,先对sharedObject.nonVolatile变量进行写操作,所以当线程A要将volatile的sharedObject.counter写回到主存时,这两个变量都会被写回到主存中。


    同理,线程B在读取volatile变量到sharedObject.counter的时候,两个变量sharedObject.counter and sharedObject.nonVolatile所以线程读取变量sharedObject.nonVolatile就会看到他被线程A改变后的值。

    JVM有时候为了提高效率,可能会改变指令执行的顺序,只要JVM判断这样做不改变指令的语义,那么就有可能改变指令的顺序。那么如果JVM改变了指令的执行顺序会发生什么呢?put方法可能会像下面这样执行:


    while(hasNewObject) { 
    //wait - do not overwrite existing new object 
    } 
    hasNewObject = true; //volatile write 
    object = newObject; 

    我们观察到,现在对于volatile的hasNewObject 操作在object = newObject;之前执行,这说明,object还没有真正被赋值新对象,但是hasNewObject 已经先变为true了。对于JVM来说,这种交换是完全有可能的。因为这两个write的指令彼此不是互相依赖的。


    但是这样交换顺序之后可能会对object变量的可见性产生不好的影响。首先,线程B可能会在线程A真正给object写入一个新值之前,就看到hasNewObject 变为true。
    另一方面,我们无法确保object什么时候会被真正写入到主内存中。


    为了防止上面这种情况的发生,volatile关键字就提出了一种“happens before guarantee”,这可以保证volatile的变量的读写指令不会被重新排序。指令前面的和后面的可以随意排序,但是volatile变量的读写指令的相对顺序是不能改变的。


    看下面这个例子就能理解了:


    sharedObject.nonVolatile1 = 123; 
    sharedObject.nonVolatile2 = 456; 
    sharedObject.nonVolatile3 = 789; 
    sharedObject.volatile = true; //a volatile variable 
    int someValue1 = sharedObject.nonVolatile4; 
    int someValue2 = sharedObject.nonVolatile5; 
    int someValue3 = sharedObject.nonVolatile6; 

    JVM可能会改变前三个指令的顺序,只要他们在volatile的写指令之前发生(就是说他们必须在volatile的写指令之前发生)。
    同理,JVM也可能改变后三个指令的顺序,只要他们在volatile的写指令之后发生。


    这就是对于Java的 volatile happens before guarantee.的最基本的理解


    Volatile有时候也是不够的 

    虽然volatile可以保证读取操作直接从主内存中的读取,写操作直接写到内存中,但仍然存在一些情况下,光使用volatile关键字是不够的。


    在之前的举例的程序中,只有一个线程在向共享变量写入数据的时候,声明为volatile,另一个线程就可以一直看到最新被写入的值。


    实际上,只要新值不依赖旧值的情况下,多个线程同时向共享的volatile变量里写入数据时,仍然能在主内存中得到正确的值。换句话说,就是这个volatile变量值更新的时候,不需要先读取出他以前的值才能得到下一个值。


    只要一个线程需要先读取一个voaltile变量,然后必须基于他的值才能产生新的值,那么volatile关键字就不再能保证变量的可见性了。在读取变量和写入变量的时候,存在一个短的时间间隙,这就会造成,多个线程可能会在这个间隙读取同一个值,产生一个新值,然后写入到主内存中,将其他线程对值的改变给覆盖了。


    所以常见的情况就是如果一个volatile变量在进行自增或者自减操作,那么这时候使用volatile就可能出问题。
    接下来我们更深入的讨论这个问题,假设线程1读取一个共享的counter变量到cpu的cache中,此时他的值是0,然后给它自增加一,但是还没有写到主存中,所以主存中还是1,线程2也能够读取同一个counter变量,而这个变量读取的时候还是0,在他自己的cpucache中,这样就出现问题了:


    深入理解Java多线程中的volatile关键字 

    线程1和线程2实际上是不同步的。共享变量counter的真实值实际上应该为2,因为被加了两次,但是每个线程在自己的cache上存的值是1,而且在主存中这个值仍然是0,这就变得很混乱。即使线程最后将值写回到主存中,但最后的值也是不正确的。


    什么时候volatile足够了 

    前文中提到,如果两个线程都在对volatile变量进行读写操作,那么仅仅使用volatile关键字是远远不够的。你需要使用synchronize关键字,来保证读写操作的原子性。


    volatile关键字对效率的影响 

    读写一个volatile变量的时候,会导致变量直接在主存中读写,显然,直接从主存中读写速度要比从cache中来得慢。另一方面,操作volatile变量的时候不能改变指令的执行顺序,这一定程度上也会影响读写的效率。所以,只有我们需要确保变量可见性的时候,才会使用volatile关键字。

  • 但如果是只有一个线程在读写volatile变量,另外的多个线程仅仅是读取这个变量的话,那么这就可以保证,其他读线程所看到的变量值都是最新的。volatile关键字可以使用在32位或者64位的变量上。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值