前言:
关于java中的volatile关键字,是之前自己一直不太自信的一个知识点,平常我们可能在写单例时经常能看到:
而不管是面试还是工作,对于彻底理解它意义是非常之大的,而要彻底理解这个关键字也不是非常容易的事,牵扯到的知识点还是很多的,所以接下来则透析这个关键字。
volatile关键字作用:
volatile英文的意思是"不稳定的", 它主要有三方面的作用:
1、实现long/double类型变量的原子操作。
2、防止指令重排序。【这是平常我们都有听说过的】
3、实现变量的可见性。
解析“实现long/double类型变量的原子操作”:
对于long和double原生数据类型都是占8个字节,也就是64位, 对于Java的原生类型还有其它6种,为啥单单只提到long和double呢?因为除了这俩原生类型,其它的原生类型不管是变量的读写都是原子性的,比如说:int a = 1;,它就是一个原子操作,也就是线程安全的。但是!!对于long和double类型它们却不是原子的,怎么理解,下面用一图来阐述一下为啥不是原子的:
如图,这里以long类型来进行说明,总共是64位,而实际在地址上是分为低32位和高32位来表示的,对于计算机,有32和64位的处理器,像32位的机器很显然无法寻址到64位地址,那对于这样的机器对于long类型是如何来处理的呢?比如以写动作为例:
double a = 1.0;
它在写入时是先写入低32位的数字,再写入高32位的数字,然后再将高低32组合起来就变成了最终值了,所以很明显这种写操作不是原子性的,分步骤了嘛,所以如果在多线程的环境下,此时非原子的操作就会产生问题了,下面来分析一下会产生啥问题:
此时又有一个线程来读取long数据,此时这个线程读到的是新的低32位的数据+旧的高32位的数据,是不是最终读出来的结果肯定就不如预期了。这是多线程在写读的情况下的问题,而如果在多线程同时写的情况下也是会存在问题的:
好,此时线程2又准备来写低32位了,此时就变成这样了:
此时,线程1再准备写高32位数据时,是不是整个数据就乱了,再读的话,就是线程1和线程2的一个中间结果,这就是对于long和double这俩数据类型的一个非常严重的问题,此时要解决这个问题,就可以用volatile关键字声明既可:
volatile double a = 1.0;
它就能保证a这个double变量的一个原子性,这也是volatile关键字的一个很重要的作用之一,当然对于并发包中出现一个针对long的保证原子操作的类AtomicLong:
volatile关键字对硬件上的影响:
这是必须要理解的前提条件,这里再稍加阐述一下背景:在JVM当中,如果不用volatile修饰变量的话,程序在读取该变量时往往不会直接从内存当中读取,而是从cpu的寄存器中读取,因为寄存器是CPU直接可以操纵最快的途径,而内存要比寄存器要慢得多,如果没有volatile修饰的变量由于不是直接从内存当中读取的,所以有可能读取的值不是最新的值;而当使用volatile修饰变量时,应用就不会从寄存器中获取该变量的值,而是从内存(高速缓存)中获取,这样就能保存每次读取的都是最新的,因为直接是从内存中读的,但是肯定会损失一些性能,毕境比从寄存器中读要慢一些。
volatile跟锁关系:
在有些文献当中将volatile关键字是一个“轻量级的锁”,为啥?因为在某些场景下volatile关键字和锁有一些类似的地方。类似的有以下两点:
1、确保变量的内存可见性。
2、防止指令重排序。
既然类似那直接用volatile来实现锁操作不就可以了么?其实还是有不同的点的:
1、相比锁,volatile可以确保对变量写操作的原子性,但是它不具备排他性(像synchronized关键字就有排他性,所谓排他性就是同一时间只能有一个线程进行上锁,其它线程只能进行等待)。
2、使用锁可能会导致线程的上下文切换(内核态与用户态之间的切换),而使用volatile并不会出现这种情况。
volatile使用场景:
虽说volatile可以称之为“轻量级的”锁,但是!!它不能取代锁,因为它自身有一些难以解决的问题存在,什么问题呢?下面进一步阐述一下:
int a = b + 2;
像上面这句代码会产生几个指令呢?其实是会产生两个指令,第一个指令是b+1,而第二个指令是将b+1的值赋值给a,很明显不是原子性的操作。那咱们用一下volatile呗:
volatile int a = b + 2;
这样就能保证原子操作了么?no!!!!因为对于等式右侧的"b+2"这个可以被多个线程访问,那a的值也就有不确定性了,如如果这样修改呢?
volatile int b = 1;
volatile int a = b + 2;
也不行,虽说b是原子性了,但是“b + 2”还不是呀。那再看一个等式:
valatile int a = a++;
也不能确保a变量的原子性,因为a++这本身就不是原子的,先加再赋值两步操作,所以对于这种赋值操作右侧不是原子性的情况不适合使用volatile,而正确的使用姿势应该是这样:
volatile int count = 1;
volatile boolean flag = false;
下面再来看一个等式;
volatile Date date = new Date();
由于new Data()它背后是先在堆中开辟空间,然后最终返回一个引用赋值给变量date,也不是一个原子的,这里只能保证引用赋值操作是原子的,所以此时的volatile关键字保证不了原子性。
总结:
如果要实现volatile写操作的原子性,那么在等号右侧的赋值变量中就不能出现被多线程所共享的变量,哪怕这个变量也是volatile也不可以。
关注个人公众号,获得实时推送