Volatile的原理:volatile变量修饰的共享变量进行写操作时会在汇编代码前加上lock前缀,lock前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 该写回内存的操作会使在其他CPU里缓存了该内存地址的额数据无效
Volatile特性之一:保证线程之间的可见性
保证变量在线程之间的可见性。可见性的保证是基于CPU的内存屏障指令,被JSR-133抽象为happens-before原则。
Volatile特性之二:禁止指令重排
阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。
Volatile特性之三:不保证原子性
public class Test {
public static void main(String[] args) {
Mythread mythread = new Mythread();
for(int i = 0; i < 6666; ++i){
new Thread(() -> {
try {
mythread.increment();
} catch (Exception e) {
e.printStackTrace();
}
}, "test").start();
}
System.out.println("Thread-" + Thread.currentThread().getName() +
" current num value:" + mythread.num);
}
}
class Mythread{
volatile int num = 0;
public void increment(){
++num;
}
}
可以看到,循环执行了6666次,但最后的结果为6663,说明在程序运行过程中出
现了重复的情况。
解决方案
- 使用JUC中的Atomic类(之后会专门写一篇学习笔记进行阐述)
- 使用synchronized关键字修饰(不推荐)
- 主内存(Main Memory)
主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。 - 工作内存(Working Memory)
工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。
什么时候适合用volatile呢?
- 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束