【面试宝典】彻底理解volatile
volatile关键字
原子性
众所周知,原子操作是不可被线程调度机制中断的操作。因此一个原子操作一旦发生,就一定会在任何可能的”线程上下文切换“之前完成。
在java语言中,除了long和double外原子性可以应用于其他基本数据类型的操作上。读写操作都会当作原子操作来对内存进行操作。因为JVM会将64位变量的读写拆分为两个32位的原子操作来进行,在和过程中可能会发生上下文切换,从而导致不同的task可能看到不正确结果的可能性。
如果想要实现对于long和double类型的原子性,就需要借用关键字volatile。
JMM
JMM 全称 Java Memory Model,翻译为 Java 内存模型。JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
JMM中的 happen-before规则,定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。
指令重排
编译器以及CPU为了优化代码或者执行效率而执行的优化操作;应用条件是单线程场景下,对于并发多线程场景下,指令重排会产生不确定的执行效果。
懒汉式单例模式:
public class VolatileDemo {
private static VolatileDemo instance = null;
private VolatileDemo() {
}
public static VolatileDemo getInstance() {
if (instance == null) {
synchronized (VolatileDemo.class) {
if (instance == null) {
instance = new VolatileDemo();
}
}
}
return instance;
}
}
其中 instance = new Singleton();语句在JVM中会转换成多条指令:
分配对象的内存空间
初始化对象
设置instance指向刚分配的内存地址
当有A,B两个线程时,指令经过重排序后可能会变成如下顺序:
1 ----> 3 ---->2
可以看到指令重排之后,先进行了内存地址的分配,而对象初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得instance对象进行使用就可能发生错误。
内存可见性
可见性指的是,当一个线程对共享变量(一个变量在多个线程的工作内存中都有副本)做修改时,能够及时的被其他线程看到。
代码如下:
public class VolatileDemo {
static int a, b, e, d;
public static void addOne() {
a += 1;
b += 1;
d += 1;
e += 1;
}
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
public void run() {
while (a < Integer.MAX_VALUE) {
addOne();
}
}
});
one.start();
while (true) {
if (e > b) {
System.out.println(b + "," + e);
}
}
}
}
输出结果片段如下
21308,22052
31454,31592
32768,32768
34355,34425
35454,35528
36502,36553
37436,37484
38305,38356
共享变量a、b、d、e四个数。在线程one中进行+1操作,主线程中读取e和b进行判断。如果是单线程情况顺序执行e > b肯定是假命题。但是由于存在指令重排和多线程下共享变量数据同步不一致的问题,可能会出现e > b的情况。
Volatile的引入
有了指令重排和内存可见性的问题,使得我们在多线程并发环境中,程序会出现各种各样异常情况。因此volatile就非常重要了
volatile特性
- 禁止进行指令重排序。(有序性)
- volatile 只能保证对单次读/写的原子性。(原子性)
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,那么新值对其他线程来说是立即可见的。(可见性)
volatile的内存可见性和有序性实现
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。如果将一个域声明为volatile的,那么只要对这个域产生写操作,那么所有的读操作就可以看到这个修改。即便使用本地缓存,也会将这个域写入主存。
有序性保证:volatile会影响指令重排。禁止局部指令重排。具体的可以了解下happen before,这里就不赘述了。
volatile和synchronize
volatile解决多线程共享变量可见性问题,类似于synchronize,但是不具备synchronize的互斥性。因此使用volatile而不是synchronize的唯一安全可靠的情况是类中只有一个可变域。
当一个域的值依赖于它之前的值,或者是依赖于其他域的值,volatile就无法工作了。
volatile性能问题
由于volatile每次操作都需要同步主存,因此性能开销比较大,会使得线程速度比较慢。所以在实际开发中优先考虑使用synchronize。