笔者去年面试过几家公司,基本上每家公司都会问到volatile,甚至有的公司每轮面试的时候都会问到。面试官这么喜欢问volatile就是因为这个关键字涉及到的知识点较多比如Java内存模型、内存屏障、happen-befor等知识,可以继续挖掘到系统指令、超线程等知识。
Java内存模型(JMM)
volatile是Java虚拟机提供的最轻量的同步机制,但很难被正确的理解与使用,通过学习Java内存模型对volatile专门定义的一些特殊访问规则,或许会对理解volatile有一定帮助。
Java内存模型定义了线程和内存之间关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读 / 写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在;它涵盖内存、缓存、寄存器以及其他的硬件和编译器优化。Java的内存模型抽象如下:
volatile的语义
volatile主要提供了两种语义:
1,可见性:
可见性是指一个线程写入的值,其他线程能够立即读取。在由Java内存模型可知道,每个线程都是有本地内存。所以线程A写入在正常情况下,线程B不能立即读取。但是在volatile变量,可以保证线程A不写入本地内存直接写入主内存,线程B直接从主内存中读取,不从本地内存中读取。
2,禁止指令重排序:
重排序是指编译器和处理器为了优化程序性能而对指令进行重排序的一种优化手段。
Java程序的几种重排序
-
编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
-
指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
-
内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
volatile的技术基石--内存屏障
内存屏障是cpu指令,该指令保证特定操作的顺序性和某些内存的可见性。插入一条内存屏障指令之后会告诉编译器和CPU:不管什么指令都不能和这条指令重排序。内存屏障所做的另外一件事情就是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入cache的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。
对于Java程序而言,如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令。
volatile的典型用例
状态标志,代码示例如下:
线程1执行run()的过程中,可能有另外的线程2调用了shutdown,所以stop变量必须是volatile(利用的volatile的可见性)。
还有一种常见的用法在双重检验的单例实现上,代码如下:
instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
-
给 instance 分配内存
-
调用 Singleton 的构造函数来初始化成员变量
-
将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
如果instance变量没有加volatile,因为指令重排序的存在,就可能导致执行步骤是1-2-3,也可能是1-3-2。一旦是1-3-2,就可能会导致访问未初始化的内存。但是加上volatile关键字之后,一定保证是按照1-2-3步骤执行的(利用的volatile的禁止重排序)。