Java中volatile的理解
面试中经常被问到的volatile
,曾经很讨厌,现在很喜欢 哈哈
《深入理解JVM》那本书相信很多人在准备面试的时候都翻过,也看到过对volatile
关键字的解释,但我理解了很多遍,终于在这轮面试接近尾声的时候才觉得理解的比较透彻了,在此做个小总结
volatile的两个语义
在java里,volatile
主要包含了线程间可见性和禁止指令重排序优化,这两个特性很特别,既不像普通变量理解简单,又不像synchronized
一样简单粗暴,它看起来和线程安全有关系,又不能保证线程安全,刚开始理解起来确实有些难度。
线程间可见性
volatile
在java里的第一个特性叫线程间的可见性,要理解这个问题,首先需要了解java内存模型
java内存模型简述
这里的java内存模型指屏蔽掉各种硬件和操作系统的内存访问差异。
简单来说,java的内存分为工作内存和主内存两种,每个线程拥有自己的工作内存,共享的变量会通过主内存去进行同步。
- 举个例子,一个共享的变量
value
在工作内存中初始化,并同步到主内存中,另外一个线程就可以通过主内存去访问到这个变量。 - 每次读取操作前会从主内存中获取该变量的值,写操作之后将值同步到主内存的方式来进行共享
java内存中的几种原子操作
- 用于主内存的:
lock()
,unlock()
- 用户工作内存的:
use()
,assign()
- 用于同步的:
- 从主内存到工作内存:
read()
,load()
- 从工作内存到主内存:
store()
,write()
- 从主内存到工作内存:
这几种方法是内存模型中的原子操作,java内存模型保证了read()
和load
,store()
和write()
的顺序执行,但注意并不是连续执行
volatile如何保证线程间变量的可见性
普通变量通过主内存去共享变量的时候,并不能保证A线程修改完该值之后立马同步回主内存,也不能保证读取该变量之前一步进行变量值的读取。volatile
变量保证:
- 使用变量前必须先从主内存刷新最新的值 ->
use()
前必须执行read(),load()
中间不能有其他操作 - 修改变量的值后立刻同步回主内存 ->
assign()
后连续执行store(),write()
通过上述规则,volatile
变量保证了其线程间的可见性。但是,这样并没有就等于说volatile
变量就保证了线程安全性。举个例子:1
2
3
4
5
6
7
8
9volatile int value = 0;
//in thread-1
for(int i=0; i<10000; i++){
value ++;
}
//in thread-2
for(int i=0; i<10000; i++){
value ++;
}
对于上面的代码,如果是线程安全的,最后的value
值应该为20000
,但是如果测试一下的话会发现,每次都要比20000
少。因为volatile
变量每次只保证取到了最新的值,例如说100
,A,B线程同时取到的都是100
,进行自加操作后,都成了101
然后同步回主内存也为101
,自然就少了一次自增操作。因此volatile
变量并不能保证线程的安全性
因此volatile
变量适用于:
- 运算结果不依赖变量当前的值,或者能够确保只有单一的线程可以修改变量的值
volatile boolean flag = true
就可以保证线程的安全性
- 变量不需要与其他变量共同参与不变约束
禁止指令重排序优化
第二个语义一直是理解的难点,结合了happens-before
原则,这次有了更清晰的了解。
指令重排序优化的意思是什么呢?
- 当我们在一个线程里观察该线程内的操作的时候,它表现出来的执行顺序看起来和程序代码的顺序是相同的
- 但是当我们在另外一个线程去观察本线程内的操作的时候,一切都是无序的操作,因为编译器可能会对我们写出来的代码进行重排序优化
同样,举个双校验模式的单例来说:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton{
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance() {
if(uniqueInstance == null) {
synchronized(Singleton.class) {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
在这里,new Sinlgeton()
看起来只有一句,但是被编译成指令是3句,分别是
- 为
uniqueInstance
分配内存空间 - 调用
uniqueInstance
的构造函数,初始化成员变量 - 为
uniqueInstance
赋值
根据前面的介绍,步骤2 和3 可能会被重排序,这样的话可能某个线程获取到的单例就是未完全初始化的实例。如果有volatile
关键字,就可以禁止进行指令重排序优化了,保证了单例的正确运行。
java内存模型中的volatile应用
根据上面的介绍,我们对volatile
有了一定的理解,那这个关键字在内存模型中起到一个什么作用呢?
首先,java内存模型主要是围绕三个特性进行设计的:
- 原子性
- 可见性
- 有序性
分别来说, 原子性即指前面介绍的8种操作(read()load()
等),例如synchronized
底层就是lock(),unlock()
实现
保证可见性即通过synchronized
,volatile
以及final
修饰的变量(不可变即线程安全了)
最后一个有序性也就是前面说过的,当我们在一个线程里观察该线程内的操作的时候,它表现出来的执行顺序看起来和程序代码的顺序是相同的,当我们在另外一个线程去观察本线程内的操作的时候,一切都是无序的操作,因为编译器可能会对我们写出来的代码进行重排序优化。 这里涉及到happens-before
原则,感兴趣的同学可以去翻一下JVM那本书再了解一下
总结
本来以为三两句能说完,原来还是涉及的东西比较多,要深入理解的话,至少要理解这些内容,不知不觉就扯了这么多。 面试的时候,通常都是单例模式惹的祸,所以这块儿是个隐藏的坑,如果要说单例,千万要对volatile
有个深入的了解。