预备知识
在了解volatile关键字之前,先了解并发编程中的可见性、原子性和有序性。
- 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。 即,一个线程修改的结果,另一个线程马上就能看到。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。除了volatile关键字,synchronized和Lock都可以保证可见性。
- 原子性,是一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如a=1
是原子性操作,但是a++
和a+=1
就不是原子性操作。Java中的原子性操作为:
- 基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
- 所有引用reference的赋值操作。
java.concurrent.Atomic.*
包中所有类的一切操作。
- 有序性,程序执行的顺序按照代码的先后顺序执行。
Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指**“指令重排序”现象和“工作内存主主内存同步延迟”现象。**
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果, 但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。除了volatile关键字,synchronized和Lock也可以保证有序性。
指令重排是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
- 重排序操作不会对存在数据依赖关系的操作进行重排序。比如:
a=1;b=a;
这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。- 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:
a=1;b=2;c=a+b;
这三个操作,第一步(a=1
)和第二步(b=2
)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b
这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3
。重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,发生重排序可能会影响结果。比如:有两个线程A和线程B,当A执行
init()
方法时发生了指令重排,2先执行,这时线程B执行use()
方法,这时我们拿到的变量a却还是0,所以最后得到的结果i=0
,而不是i=1
。
volatile存在的原因
JMM决定了一个线程对共享变量的写入合适对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。
volatile变量的特性
- 保证可见性,不保证原子性
- 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去。
- 这个写会操作会导致其他线程中的volatile变量缓存无效。
-
禁止指令重排
- 使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
volatile禁止指令重排序也有一些规则:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
volatile原理
在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存无效。
volatile常见的面试题目
- 单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!synchronized和volatile关键字不是都可以保证有序性吗?
① 单例设计模式,就是采取一定的方法保证在整个的软件系统中,某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。常见的单例模式使用方式有八种:饿汉式(静态常量)、饿汉式(静态代码块)、懒汉式(线程不安全)、懒汉式(线程安全,同步方法)、懒汉式(线程不安全,同步代码块)、双重检查、静态内部类、枚举。
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance == new Singleton();
}
}
}
return instance;
}
}
② 上面的方法中使用synchronized
关键字,确保只能有一个线程去创建实例,而第二个if
语句确保多个获得锁的线程不会重复创建对象。但由于对象创建是分为三步执行得:①JVM为对象分配一块内存空间;②在内存空间上对对象进行初始化;③将内存空间的地址复制给instance
变量,这三步的执行顺序可能是①②③,也可能是①③②,如果为后者,那么新加入的线程可能会拿到一个未进行初始化的对象。
③ synchronized
的有序性是持有相同锁的两同步代码只能串行加入,即被加锁的内容要按照顺序被多个线程执行,但是其内部的同步代码还是会发生指令重排序,使块与块之间有序可见。volatile
的有序性是通过插入内存屏障来保证指令按照顺序执行。不会存在把后面的指令跑到前面的执行之前来执行,是保证编译器优化时不会让指令重排序。
参考文献: