volatile主要用来解决线程的可见性,有序型和原子性问题(synchronized和锁也可,volatile更轻量)。
JSR中对volatile的说明:
The Java programming language allows threads to access shared variables. As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.
A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.
可见性
CPU在计算的时候,它的数据读取顺序优先级是:寄存器-高速缓存-内存。线程计算的原始的数据来自内存;在计算过程中有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题(线程A回写数据到主存,但是其他线程仍使用自己工作内存数据)。
volatile如何保证可见性:
- 修改volatile变量时会强制将修改后的值刷新的主内存中。
- 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
当线程读取volatile修饰的变量,就好比JVM告诉他这个变量是不安全的,需要从主存重新获取。
原子性
JVM中变量的读写操作一般是原子性的。
- 基础数据类型(除了double, long)和引用类型都是原子性操作
For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half.
- volatile + 基础数据类型/引用类型 都是原子性操作
注意:volatile无法保证复合操作的原子性,例如 i++; 正确使用volatile条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
volatile boolean flag;
public void doSth(){
while(flag){
dosomething();
}
}
public void setFlag(){
flag = true;
}
有序性
为了提高性能,编译器和处理器可能会对指令做重排序。重排序可以分为三种:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
volatile和synchronized可以保证有序性。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
volatile底层实现
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
引用
java内存模型
Java 并发编程:volatile的使用及其原理
java线程内存模型,线程、工作内存、主内存
The Java® Language Specification
Java并发编程:volatile关键字解析
java 里面保留字volatile及其与synchronized的区别
《深入理解Java内存模型》读书总结
The JSR-133 Cookbook for Compiler Writers