并发编程有三要素,原子性、有序性和可见性,volatile就占了后面两个。那么他是如何保证后面两个特性的呢?
特性
保证可见性、有序性,不保证原子性。
可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指 令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程 对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理 器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过 期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存 行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内 存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是 从主内存中获取最新的。
有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会 影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile所做的就是禁止指令重排。
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时 不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障 的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
应用理解
可见性
volatile boolean isEnd = false;
while(isEnd){
// doSomeThing...
}
这里的volatile就是保证了可见性,在另一个线程中,当isEnd内容改变的时候能及时通知到循环所在的线程。如果不加,另一个线程中的isEnd变量在改动之后,会先存放在线程的内存中,然后去做其他的事情,结束这个操作的实效就不得而知了。
有序性
public class Singleton{
private static volatile Singleton instance = null;
private Singleton(){}
public Singleton getInstance(){
if(null == instance){
synchronized(Singleton.class){
if(null == instance){
instance = new Singleton();
}
}
}
return instance;
}
}
为什么需要两次check?
-
如果没有外层的check,相当于给整个getInstance()方法加上了synchronized关键字,也就是每次获取单例对象都要获取class对象的monitor,monitor是粒度较大的的锁,开销较大。所以外层的判断目的是:第一次获取单例对象后,再次获取该单例对象无需进行同步
-
如果没有内层的check,假如有两个线程,线程1和线程2同时进入外层判断,即第8步,线程1获得对象锁,进入同步代码块并初始化对象后,释放对象锁,返回单例对象结束了,线程1获取对象锁进入同步代码块后又再次初始化了instance对象,导致多线程下单例模式的非线程安全;
为什么instance实例需要加volatile关键字?
-
因为volatile的禁止指令重排序,初始化instance对象并非原子操作,它包括: 1. 开辟堆内存; 2. 调用构造方法初始化对象; 3.将instance指向新对象。
如果没有volatile关键字,且在并发情况下,如果某个线程完成了1、2两个步骤,还未给instance变量赋值,此时另一个线程进入外层判空后后发现instance对象非空,就返回了未构造完全的instance对象,导致空指针异常;volatile的意义在于能够禁止对当前对象进行指令的重排序,也就是happen-before原则的关于volatile的一条:"volatile变量规则:对一个变量的写操作happen before于后面对这个变量的读操作",也就是说,无论什么情况,对于volatile变量的写操作必须在完成后才能读取,不能暴露写操作的中间状态。所以不会出现未完成构造就读取的情况;但是volatile不能保证同时对变量的写操作也是有序的,也就是volatile不能保证原子性
happen before:描述了线程安全的可见性,有很多规则,关于volatile的规则是:对一个变量的写操作的结果,对这个变量的读操作可见
网友评论