volatile能够保证线程可见性,当一个线程修改主内存共享变量时能够保证对于另外一个线程是可见的,但是不能保证共享变量的原子性问题。
源链接 volatil原理分析-博客园
volatile特性
可见性
能够保证线程可见性,当一个线程修改共享变量时,对于另外的使用该变量的线程是可见的。
顺序性
防止指令重排序
volatile可见性
public class VolatileTest extends Thread {
/**
* volatile关键字底层通过 汇编 lock指令前缀 强制修改值,并立即刷新到主内存中,另外一个线程可以马上看到刷新的主内存数据
*/
private static volatile boolean FLAG = true;
@Override
public void run() {
while (FLAG){
try {
TimeUnit.MILLISECONDS.sleep(300);
System.out.println("==== test volatile ====");
} catch (InterruptedException ignore) { }
}
}
public static void main(String[] args) throws InterruptedException {
new VolatileTest().start();
TimeUnit.SECONDS.sleep(1);
FLAG = false;
}
}
CPU多核硬件架构剖析
CPU的运行速度非常快,而对磁盘的IO速度却很慢,为了解决这个问题,有了内存的诞生;而CPU中寄存器的读写速度和内存的读写速度仍然有着100:1的差距,为了解决这个问题,又在寄存器和内存之间建立了多级缓存:L1、L2、L3三级缓存。
产生可见性的原因
当CPU读取主存中共享变量时,效率是非常低的,所以L1、L2、L3缓存中会缓存主存中的共享变量的副本。由于多级缓存的存在,就可能存在缓存不一致的情况,导致多个线程同时修改一个共享变量时无法读取到真实值,从而导致可见性问题。
JMM内存模型
Java内存模型定义了一种抽象的概念,用来屏蔽java对不同的操作系统内存的访问差异。
主内存:共享变量所在数据区域
工作内存:每个线程所分配的内存区域,各线程之间彼此隔离,每个线程都有一份共享变量的副本,这是发生可见性问题的原因。
双重校验机制为什么需要volatile
public class Singleton {
private volatile static Singleton Singleton;
//构造函数私有化
private Singleton() {
}
public static Singleton getSingleton() {
synchronized (Singleton.class){
if(Singleton == null){
Singleton = new Singleton();
}
}
return Singleton;
}
}
注意:
在声明private volatile static Singleton双重校验时,如果去掉volatile关键字,我们在new 操作时存在重排序的问题。
getSingeleton获取对象的过程可分为如下三步:
- 分配对象的内存空间
- 调用构造函数初始化
- 将对象复制给变量
如果没有使用volatile关键字修饰,则第二步和第三步存在重排序问题,有可能先将对象复制给变量,再调用构造函数初始化,如此则将导致另外一个线程获取该对象不为空,但是构造函数没有初始化的半初始化对象,将导致报错,也即另外的线程得到的是不完整的对象。