volatile如何实现可见性

引言

  在描述volatile变量有什么作用的时候,经常会提到volatile可以使线程看到最新的变量值。但是进一步问到为什么能看到最新值的时候,会说volatile会使得线程的缓存无效,写会直接更新主内存中的值,读也会直接从主内存中读。本来这样的解释也就可以了,但是最近在解释同学的一个问题的时候还是觉得自己没搞清楚。问题如下:

“Java原子类中,CAS操作可以使得主内存中的值是最新值,那为什么还要把value声明为volatile?”

  这个可以用之前的线程缓存无效来回答(目前是这么认为的),但是这个线程缓存到底是什么那?和Java栈又是什么关系那?是如何实现缓存无效的那?这些问题实际上在《深入理解Java虚拟机》中的12.3Java内存模型这一节中有了描述,这里再把这些内容描述一下,只是为了加深一下印象。

内存模型

  与C/C++等语言直接使用操作系统的内存模型不同,Java虚拟机规范中定义了一种Java内存模型(Java Memory Model, JMM),该模型是基于各个平台之上的一个规范定义。这样可以屏蔽操作系统带来的差异,使得Java具有更好地移植性。Java内存模型结构如下图所示:

Java内存模型结构

  其中,线程的工作内存中保存了从主内存中拷贝的变量副本,线程操作的变量作都是从工作内存中获取的,而且各个线程的工作内存互不影响,这就构成了对主内存的一个“缓存”。所以如果线程中的缓存如果没有及时刷新的话,就会读取旧的数据。也就是说,一个线程对共享变量操作后,其它线程不一定能够及时获取到,对其它线程是“不可见的”。

工作内存

  之前提到了工作内存,那么工作内存到底是什么那?和Java栈又是什么关系那?《深入理解Java虚拟机》书中给出了一个说明:
  Java内存模型中的主内存、工作内存和Java内存区域中的Java堆、栈、方法区不是同一个层次的内存划分,这两者基本上没有基本的关系。如果要勉强进行对应,工作内存可以对应虚拟机栈中的部分区域,从更低层次来说,由于虚拟机主动实现或者是系统本身的机制,可能会让工作内存优先存储于寄存器和高速缓存中,这样可以提供访问速度(工作内存是程序主要访问读写的地方)。

线程读取变量过程

  下面描述来自此文章。
  线程读取主内存变量过程如下图所示:

线程读取主内存变量过程

  其中部分原子操作规则如下:

  1. read,load必须连续执行,但是不保证原子性。
  2. store,write必须连续执行,但是不保证原子性。
  3. 不能丢失变量最后一次assign操作的副本,即遍历最后一次assign的副本必须要回写到MainMemory中。

volatile特殊规则

  volatile可以实现工作内存缓存无效主要是因为它具有一些特殊的规则。

  1. 在use之前必须使用load,根据上面的read,load必须连续执行可以推出:如果要使用(use)变量的话,必须从主内存中获取(read和load)。
  2. assign只能在store之前使用,根据上面的store,write必须连续执行可以推出:如果要更新(assign)变量的话,必须写回到主内存中(store,write)。

  通过这两条规则,可以使得volatile声明的变量能够避免工作内存中缓存的影响,相当于直接读写主内存。当然volatile还有其它的一些规则,这里不再列出。

总结

  通过这些介绍,可以回答开题的一些问题。
  Q:这个线程缓存到底是什么那?
  A:工作内存,具体实现硬件取决于操作系统和硬件,可以是高速缓存或者寄存器。
  Q:和Java栈又是什么关系那?
  A:没有基本的关系。如果要勉强进行对应,工作内存可以对应虚拟机栈中的部分区域。
  Q: 是如何实现缓存无效的那?
  A:通过volatile的语义规则,可以使得线程一定要读取或更新内存中的值。(面试中面试官曾说通过设置Cache中的缓存行无效实现,这个回答前提应该是工作内存是通过Cache实现,该回答了进一步描述了语义规则如何通过硬件实现)。
  “你看了,但是你没看进去”。的确,有些问题深究之后才发现其实不是很懂,还是要精益求精,凡事多问问为什么。

-------------本文结束感谢您的阅读-------------