在Java的面试当中,面试官最爱问的就是volatile关键字相关的问题。通过屡次面试以后,你是否思考过,为何他们那么爱问volatile关键字相关的问题?而对于你,若是做为面试官,是否也会考虑采用volatile关键字做为切入点呢?c++
爱问volatile关键字的面试官,大多数状况下都是有必定功底的,由于volatile做为切入点,往底层走能够切入Java内存模型(JMM),往并发方向走又可接切入Java并发编程,固然,再深刻追究,JVM的底层操做、字节码的操做、单例均可以牵扯出来。面试
因此说懂的人提问题都是有门道的。那么,先总体来看看volatile关键字都设计到哪些点:内存可见性(JMM特性)、原子性(JMM特性)、禁止指令重排、线程并发、与synchronized的区别……再往深层次挖,可能就涉及到字节码、JVM等。编程
不过值得庆幸的是,若是你已经学习了微信公众号“程序新视界”JVM系列的文章,上面的知识点已经不是什么问题了,权当是复习了。那么,下面就以面试官提问的形式,在不看答案的状况下,尝试回答,看看学习效果如何。夺命连环问,开始……缓存
被volatile修饰的共享变量,就具备了如下两点特性:微信
回答的很好,点出了volatile关键字两大特性。针对该两大特性继续深刻。markdown
该问题涉及到Java内存模型(JVM)和它的内存可见性特性,这里将前面系列《Java内存模型(JMM)详解》和《Java内存模型相关原则详解》中的部份内容整理出来回答。多线程
先说内存模型:Java虚拟机规范试图定义一种Java内存模型(JMM),来屏蔽掉各类硬件和操做系统的内存访问差别,让Java程序在各类平台上都能达到一致的内存访问效果。并发
Java内存模型是经过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,将主内存做为传递媒介。可举例说明内存可见性的过程。jvm
本地内存A和B有主内存中共享变量x的副本,初始值都为0。线程A执行以后把x更新为1,存放在本地内存A中。当线程A和线程B须要通讯时,线程A首先会把本地内存中x=1值刷新到主内存中,主内存中的x值变为1。随后,线程B到主内存中去读取更新后的x值,线程B的本地内存的x值也变为了1。函数
最后再说可见性:可见性是指当一个线程修改了共享变量的值,其余线程可以当即得知这个修改。
不管普通变量仍是volatile变量都是如此,只不过volatile变量保证新值可以立马同步到主内存,使用时也当即从主内存刷新,保证了多线程操做时变量的可见性。而普通变量不可以保证。
咱们知道JMM除了可见性,还有原子性和有序性。
原子性即一个操做或一系列是不可中断的。即便是在多个线程的状况下,操做一旦开始,就不会被其余线程干扰。
好比,对于一个静态变量int x两条线程同时对其赋值,线程A赋值为1,而线程B赋值为2,无论线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操做是没有干扰的,这就是原子性操做,不可被中断的。
在Java内存模型中有序性可概括为这样一句话:若是在本线程内观察,全部操做都是有序的,若是在一个线程中观察另外一个线程,全部操做都是无序的。
有序性是指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,由于在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致。
所以,上面概括的前半句指的是线程内保证串行语义执行,后半句则指指“令重排现”象和“工做内存与主内存同步延迟”现象。
CPU和编译器为了提高程序执行的效率,会按照必定的规则容许进行指令优化。但代码逻辑之间是存在必定的前后顺序,并发执行时按照不一样的执行逻辑会获得不一样的结果。
举个例说明多线程中可能出现的重排现象:
class ReOrderDemo {
int a = 0;
boolean flag = false;
public void write() {
a = 1; //1
flag = true; //2
}
public void read() {
if (flag) { //3
int i = a * a; //4
……
}
}
}复制代码
在上面的代码中,单线程执行时,read方法可以得到flag的值进行判断,得到预期结果。但在多线程的状况下就可能出现不一样的结果。好比,当线程A进行write操做时,因为指令重排,write方法中的代码执行顺序可能会变成下面这样:
flag = true; //2
a = 1; //1复制代码
也就是说可能会先对flag赋值,而后再对a赋值。这在单线程中并不影响最终输出的结果。
但若是与此同时,B线程在调用read方法,那么就有可能出现flag为true但a仍是0,这时进入第4步操做的结果就为0,而不是预期的1了。
而volatile关键词修饰的变量,会禁止指令重排的操做,从而在必定程度上避免了多线程中的问题。
volatile保证了可见性和有序性(禁止指令重排),那么可否保证原子性呢?
volatile不能保证原子性,它只是对单个volatile变量的读/写具备原子性,可是对于相似i++这样的复合操做就没法保证了。
以下代码,从直观上来说,感受输出结果为10000,但实际上并不能保证,就是由于inc++操做属于复合操做。
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}复制代码
假设线程A,读取了inc的值为10,然被阻塞,因未对变量进行修改,未触发volatile规则。线程B此时也读取inc的值,主存里inc的值依旧为10,作自增,而后马上写回主存,值为11。此时线程A执行,因为工做内存里保存的是10,因此继续作自增,再写回主存,11又被写了一遍。因此虽然两个线程执行了两次increase(),结果却只加了一次。
有人说,volatile不是会使缓存行无效的吗?可是这里线程A读取以后并无修改inc值,线程B读取时依旧是10。又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?只有在作读取操做时,发现本身缓存行无效,才会去读主存的值,而线程A的读取操做在线程B写入以前已经作过了,因此这里线程A只能继续作自增了。
针对这种状况,只能使用synchronized、Lock或并发包下的atomic的原子操做类。
可举单例模式的实现,典型的双重检查锁定(DCL):
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) { // 1
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton(); // 2
}
}
return instance;
}
}复制代码
这是一种懒汉的单例模式,使用时才建立对象,并且为了不初始化操做的指令重排序,给instance加上了volatile。
为何用了synchronized还要用volatile?具体来讲就是synchronized虽然保证了原子性,但却没有保证指令重排序的正确性,会出现A线程执行初始化,但可能由于构造函数里面的操做太多了,因此A线程的instance实例尚未造出来,但已经被赋值了(即代码中2操做,先分配内存空间后构建对象)。
而B线程这时过来了(代码1操做,发现instance不为null),错觉得instance已经被实例化出来,一用才发现instance还没有被初始化。要知道咱们的线程虽然能够保证原子性,但程序多是在多核CPU上执行。
固然,针对volatile关键字还有其余方面的拓展,好比讲到JMM时可拓展到JMM与Java内存模型的区别,讲到原子性时可扩展到如何查看class字节码,讲到并发可扩展到线程并发的方法面面。
其实,不只面试如此,在学习知识时也能够参考这种面试思惟,多问几个为何。将一个点,经过为何拓展成一个知识网。
原文连接:《Java面试官最爱问的volatile关键字》
《面试官》系列文章: