浅谈volatile的原理

1. 概述

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新, 线程可以通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。

volatile具有以下特性

  • 可见性:对于volatile变量,一个线程对它的修改,对其他线程是可见的。也就是说,任何线程读这个变量时,读到的都是最新值。
  • 原子性:volatile只能保证对单个变量读或写的原子性,对复合操作(比如i++)是不具有原子性的。

2. volatile的可见性

那么volatile的可见性是如何实现的呢?我们先来简单了解一下Java内存模型。(这部分内容参考《Java并发编程的艺术》)

在并发编程中,需要处理两个关键问题:线程之间如何通信、线程之间如何同步。通信是指线程之间以何种机制来交换信息,同步是指以何种机制来控制不同线程执行操作的相对顺序。Java采用的是共享内存模型,在此模型下,Java线程之间的通信是隐式进行的,整个通信过程对程序员完全透明;而同步是显式的,程序员必须显式地指定某些代码需要在线程之间互斥地进行。(这段可能比较抽象,不太理解也没关系,接着往下看)

在JVM的运行时数据区中,堆区和方法区是线程的共享区域,我们这里重点关注堆区。堆区存放着对象实例、静态于和数组元素,我们统称它们为共享变量。Java内存模型(JMM)规定,线程之间的共享变量存储在主内存中,每个线程的都有一个私有的本地内存(即该线程的工作内存),本地内存存放了该线程操作的共享变量的副本。本地内存是JMM的一个抽象概念,并不是真实存在的,它包括了缓存、写缓冲区、寄存器等。JMM的抽象结构示意图如下

对于没有被volatile修饰的变量,当线程要读共享变量时,会先判断此变量是否在CPU缓存中,如果在就从缓存读取,如果不在就从主内存读取并放入缓存中;当要写这个共享变量时,会写入到缓存中,而缓存中的数据会定期刷新到主内存中。这样就存在一个问题,对于共享变量x,假设线程A从主内存读取到x=0,将其修改为x=2,并写入缓存中,还没有刷新到主内存时,另一个线程B也来从主内存读取变量x,本来此时x已经被线程A修改为2了,但是B读到的依然是0。另一种情况是,假设线程A和B都缓存了变量x的副本x=0,现在A对其进行了修改x=2,并刷新到了主存,但是B在读取的时候依然从自己的缓存中读取,读到的是x=0。这就是因为不可见性而导致了不一致。也就是说,一个线程对共享变量x进行了修改,但是其他线程对此一无所知,依然在用原来的值。那么Java提供的volatile关键字是如何保证共享变量的可见性的呢?

volatile做了两件事

  • 如果线程对volatile变量进行了修改,那么此变量对应的缓存行会被立即刷新到主内存中;(这意味着主内存中的volatile变量总是最新值)
  • 这个刷新到住主内存的操作会使该变量在其他CPU中的缓存无效,也可以理解为使得其他线程工作内存中的该变量变的无效;(这意味着其他线程读这个变量时,不是从缓存中读,而是一定要从主内存中读)。这件事是通过MESI(缓存一致性协议)实现的。

那么,CPU是怎么发现这个数据失效了呢?答案是--嗅探

处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存数据在总线上保持一致。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态。

3. volatile的应用场景

要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

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

这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。当一个变量的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个变量的值受到其他变量的值限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。

场景1:状态标志

volatile boolean shutdownRequested;
...
 
public void shutdown() { 
    shutdownRequested = true; 
}
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

场景2:开销较低的“读锁”策略

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    //读操作,没有synchronized,提高性能
    public int getValue() { 
        return value; 
    } 
 
    //写操作,必须synchronized。因为x++不是原子操作
    public synchronized int increment() {
        return value++;
    }

4. volatile和synchronized的区别

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
  • volatile保证数据的可见性,仅保证了对volatile变量读和写操作的原子性;而synchronized是一种排他(互斥)机制,保证可见性和原子性。
  • volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值