volatile是Java并发编程的关键字之一,它用以修饰一个共享变量。它的作用非常强大,Java的JUC包中大量的使用了volatile,本文我们就来看下volatile到底是何物。

1. volatile是什么?

       这里引用Java语言规范第3版中声明:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。 volatile就是Java语言对这个声明的支持,如果一个变量被声明成volatile,Java内存模型确保所有线程能看到这个变量的值是一致的。

2. volatile的工作原理

       分析上面谈到的声明,可以拆分成两点:共享变量能被准确一致地更新和线程通过排他锁单独获得这个变量。这两点刚好对应了并发编程的两个关键问题:线程之间的通信和同步控制。通过前面章节我们已经知道了Java采用了两大并发模型中的共享内存模型来设计了Java内存模型,共享内存模型通过共享变量状态的变化实现线程间的通信,通过显式的指令实现线程间的同步控制。

       volatile在共享内存模型下究竟是如何做的呢?

       一段单个volatile变量写操作的代码最终转换成汇编代码时会多出一条Lock前缀的指令,多出来的指令一定是要发挥作用的,它发挥的作用就是volatile具有的作用。

       接下来看下Lock前缀的指令是如何发挥作用的。

2.1 Lock前缀指令的作用

       Lock前缀的指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。但是在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线的开销比较大。这些最近的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区域并回写到内存,并使用缓存一致性机制确保修改的原子性,此操作称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

       我们看Lock前缀的指令的这些操作,内存总线锁定和缓存锁定都确保了操作写操作的线程独占性,也就是线程之间的同步控制,比如volatile实现了double和long类型变量的原子操作。另外缓存锁定机制迫使修改的缓存数据写会内存,这实现了共享变量对其他线程可见的一部分工作,也就是说完成了一部分的通信工作,消息已经发出了,但是其他线程还没有完成接收。

       消息的接收是通过处理器在访问缓存时遵循特定的协议完成的。IA-32处理器和Intel64处理器使用MESI控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32和Intel64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。如果处理器嗅探到其他处理器修改共享变量,那么执行嗅探的处理器将使自身的缓存行置为无效,在下次访问缓存行发现失效时,重新从内存中读取数据到缓存行。这样嗅探处理器就获得其他处理器修改后的值,相当于收到了其他处理器发出的消息。

       这样并发的两大关键问题通信和同步都实现了,那volatile实现这些就够了吗?问了就是不够!在前面的章节我们了解到编译器重排序和处理器重排序会导致多线程程序执行的结果无法预测,Java内存模型阻止了特定类型的重排序来解决这个问题,如happens-before规则中的volatile规则:对一个volatile域的写,happens-before于任意一个后续对这个volatile域的读。也就是说volatile还具备阻止指令重排序的作用,发挥该作用依赖于内存屏障。

2.2 内存屏障的作用

       内存屏障也是计算机的指令,现在我们看下内存屏障的分类和在它在volatile的使用。

2.2.1 内存屏障的类型

       内存屏障分为4种类型,如下:

屏障类型指令示例说明
LoadLoadBarriersLoad1;LoadLoad;Load2确保Load1数据的装载先于Load2及所有后续装载指令的装载
StoreStoreBarriersStore1;StoreStore;Store2确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
LoadStoreBarriersLoad1;StoreStore;Store2确保Load1数据装载先于Store2及所有后续存储指令刷新到内存
StoreLoadBarriersStore1;StoreStore;Load2确保store1数据对其他处理器可见(刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoadBarriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处 理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

2.2.2 volatile如何使用内存屏障

       下面是是JMM对volatile使用内存屏障的策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • 在每个volatile读操作的后面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障

       上述内存屏障的使用策略非常保守,但是它可以保证在任意处理器平台,任意的程序中都能得到正确的发挥volatile的作用。因为JMM的一个实现特点是首先保证正确性,然后再去追求执行效率。

       下面是偷的两张图,直观看下内存屏障在volatile中的使用。

好好学习Java并发 三、关键字volatile_Java好好学习Java并发 三、关键字volatile_Java_02

3. voaltile作用小结

       通过上面内容我们了解到volatile发挥作用依赖的计算机的指令,被volatile修饰的变量被操作的时候会在相应的为之插入lock前缀的指令和内存屏障。我们可以理解为volatile是Java提供给我们实现Java并发编程的工具,它封装了底层的实现,程序员按照volatile的使用说明就能处理某些并发编程的问题。

       对程序员最重要的还是volatile的实际作用,可以知道我们的开发工作,这里总结为以下来两点(这里只总结可以指导开发的作用):

  • 保证long、double类型变量的基本操作,但是不保证复合操作的原子性
  • volatile不是锁,但是具备锁的部分能力,在一些场景下可以代替锁发挥作用,但是性能比锁要好

参考文献 《并发编程的艺术》