Java精通并发-volatile关键字作用与锁的关系深入详解

前言:

关于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也不可以。

关注个人公众号,获得实时推送

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

webor2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值