最近看到一篇很好的 volatile 可见性原理总结,分享给大家!
volatile 是一种轻量且在有限的条件下线程安全技术,它保证修饰的变量的可见性和有序性,但非原子性。相对于 synchronize 高效,而常常跟 synchronize 配合使用。
先简单说一下 Java 内存模型。这里主要描述的线程,工作内存,主存的变量的读写关系:
volatile 可见性的特殊性。
由特性性保证了 read、load 和 use 的操作连续性,assign、store 和 write 的操作连续性,从而达到工作内存读取前必须刷新主存最新值;工作内存写入后必须同步到主存中。读取的连续性和写入的连续性,看上去像线程直接操作了主存。
volatile 是非原子性的,即它不具备原子性。
volatile 的有序性,volatile 能够禁止指令重排。
指令重排是指:为了提高性能,编译器和和处理器通常会对指令进行指令重排序。
上图中的三个重排位置可以调换的,根据系统优化需要进行重排。遵循的原则是单线程重排后的执行结果要与顺序执行结果相同。
内存屏障指令:volatile 在指令之间插入内存屏障,保证按照特定顺序执行和某些变量的可见性。
volatile 就是通过内存屏障通知 cpu 和编译器不做指令重排优化来维持有序性。
与 synchronize 的串行控制的区别:
volatile 的线程安全适用范围是有条件的。由于 volatile 的非原子性原因,所以它的线程安全是有条件的:
这两条件描述出自于《深入理解java虚拟机》。
最后做个总结:
看完上面的内容,你真的懂了 volatile 吗?下面我们在看几个 volatile 常见的面试题吧。
1、volatile 关键字在 Java 中有什么作用?
volatile 是一个特殊的修饰符,只有成员变量才能使用它。
在 Java 并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。
volatile 变量可以保证下一个读取操作会在前一个写操作之后发生。
2、面试官: 继续,说说你对 volatile 关键字的理解。
就我理解的而言,被 volatile 修饰的共享变量,就具有了以下两点特性:
3、面试官: 能不能详细说下什么是内存可见性,什么又是重排序呢?
Java 虚拟机规范试图定义一种 Java 内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让 Java 程序在各种平台上都能达到一致的内存访问效果。简单来说,由于 CPU 执行指令的速度是很快的,但是内存访问的速度就慢了很多,相差的不是一个数量级,所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存。在 Java 内存模型里,对上述的优化又进行了一波抽象。JMM 规定所有变量都是存在主存中的,类似于上面提到的普通内存,每个线程又包含自己的工作内存,方便理解就可以看成 CPU 上的寄存器或者高速缓存。所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存。
在线程执行时,首先会从主存中 read 变量值,再 load 到工作内存中的副本中,然后再传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。使用工作内存和主存,虽然加快的速度,但是也带来了一些问题。比如下面这个例子:
假设 i 初值为 0,当只有一个线程执行它时,结果肯定得到 1,当两个线程执行时,会得到结果 2 吗?这倒不一定了。可能存在这种情况:
如果两个线程按照上面的执行流程,那么i最后的值居然是 1 了。如果最后的写回生效的慢,你再读取 i 的值,都可能是 0,这就是缓存不一致问题。下面就要提到你刚才问到的问题了,JMM 主要就是围绕着如何在并发过程中如何处理原子性、可见性和有序性这 3 个特征来建立的,通过解决这三个问题,可以解除缓存不一致的问题。而 volatile 跟可见性和有序性都有关。
4、面试官:你知道 volatile 底层的实现机制吗?
如果把加入 volatile 关键字的代码和未加入 volatile 关键字的代码都生成汇编代码,会发现加入 volatile 关键字的代码会多出一个 lock 前缀指令。lock 前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
5、面试官:volatile 的使用场景,请你举两个例子?
6、volatile 变量和 atomic 变量有什么不同?
首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。
Volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量那么 count++ 操作就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如 getAndIncrement() 方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
参考资料: