1)概述

这是Java提供的一种弱形式的同步,为的是在保证可见性和有序性场景下避免使用锁的笨重,它可以确保对一个变量的更新马上对其他线程可见。当一个变量被声明为volatile,那么一个线程对它做修改之后不会缓存下来,而是马上写入主内存中,之后其他线程获取值时也不会从本地内存中获取,而是从主内存中获取。此外使用volatile修饰的对象还可以避免指令重排导致的线程不安全的情况。
它和synchronized的作用是相同的,但是synchronized同一时间只允许一个线程在操作,而volatile是非阻塞的,不会造成额外的线程上下文切换开销。但是volatile也存在缺点,那就是不可用于原子操作,因此如果写入变量不依赖于变量当前值时可使用。

2)原理

汇编代码中显示,在使用volatile修饰的变量后会加上内存屏障,对于写操作的指令加的是写屏障,对于读操作加的是读屏障。写屏障后面的指令不会被重排到前面,读屏障前面的指令不会被重排到后面,由此也就保证了这部分指令的有序性;在加入内存屏障(汇编指令 lock ..)之后还捎带一个 addl$0x0,(%esp) 指令,这个操作会将本处理器的缓存写入内存中,这也就导致了其他处理器中缓存的失效。

3)应用

DCL 双重检查锁,单例模式:

  1. public class Singleton{
  2. private volatile static Singleton INSTANCE;
  3. private Singleton() {}
  4. public static Singleton getInstance() {
  5. // 第一重检查
  6. if (INSTANCE == null) {
  7. synchronized(Singleton.class) {
  8. // 第二重检查
  9. if (INSTANCE == null) {
  10. INSTANCE = new Singleton();
  11. }
  12. }
  13. }
  14. return INSTANCE;
  15. }
  16. }

这里如果不使用volatile会造成线程安全问题,因为synchronized代码块只是保证其中的不逃逸的共享数据具有原子性、可见性、有序性,但这里INSTANCE实际上已经逃逸出同步代码块的范围了,在上面的if处也使用了,因此如果不加volatile会有线程安全问题。

线程安全问题怎么产生的?
INSTANCE = new Singleton(); 实际上是几条指令的合集:

  1. 1、创建对象,将对象引用入栈
  2. 2、将对象引用复制一份
  3. 3、利用对象引用,调用构造方法
  4. 4、利用对象引用,赋值给前面的INSTANCE

这里3和4是可能发生指令重排的,也就会导致先赋值后调用构造方法,如果在赋值执行之后,其他线程执行到第一个if,那么会判定是非null的,这时会直接返回INSTANCE,而此时其代表一个并未调用构造方法的对象。