volatile关键字有以下几大特性:
- 保证内存 可见性
- 防止指令 重排序
- 保证对 64 位变量 读写的原子性
- 不能保证线程安全
保证内存可见性
什么情况下需要保证内存可见性?
在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下,volatile关键字的使用变得非常重要。
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。使用volatile修饰这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
注意:
在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
volatile做了什么?
对于 volatile
修饰的变量,JVM 可以保证:
- 每次对该变量的写操作,都将立即同步到主存;
- 每次对该变量的读操作,都将从主存读取,而不是线程栈
防止指令重排序
如果一个操作不是原子操作,那么 JVM 便可能会对该操作涉及的指令进行重排序。重排序需要按照as-if-serial语义的规则,通过调整指令的执行顺序,尽可能达到提高运行效率的目的。
as-if-serial语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
在 JDK 1.5 之后,增强了 volatile
的语义,严格限制 JVM (编译器、处理器)不能对 volatile
修饰的变量涉及的操作指令进行重排序。
保证对 64 位变量 读写的原子性
JVM 可以保证对 32位 数据读写的原子性,但是对于 long
和 double
这样 64位 的数据的读写,会将其分为 高32位 和 低32位 分两次读写。所以对于long
或 double
的读写并不是原子性的,这样在并发程序中共享 long
或 double
变量就可能会出现问题,于是 JVM 提供了 volatile
关键字来解决这个问题:使用 volatile
修饰的 long
或 double
变量,JVM 可以保证对其读写的原子性。但值得注意的是,此处的 “写” 仅指对 64位 的变量进行直接赋值。
不能保证线程安全
volatile只能保证被其修饰变量的内存可见性,但如果对该变量执行的是非原子操作线程依旧是不安全的。而对于 i++
这个语句,事实上涉及了 读取-修改-写入 三个操作:
- 读取变量到栈中某个位置
- 对栈中该位置的值进行自增
- 将自增后的值写回到变量对应的存储位置
因此哪怕变量 i
使用 volatile
修饰,也并不能使涉及上面三个操作的 i++
具有原子性。所以多线程条件下使用 volatile
关键字的前提是:对变量的写操作不依赖于变量的当前值,而赋值操作很明显满足这一前提。
总结
在多线程环境下,正确使用 volatile
关键字可以比直接使用 synchronized
更加高效而且代码简洁,但是使用 volatile
关键字也更容易出错。所以,除非十分清楚 volatile
的使用场景,否则还是应该选择更加具有保障性的 synchronized
。