深入分析 Volatile 的实现原理

整体认识

Volatile是相对于synchronized来说的轻量级锁,其最大的特点是没有通过加锁的方式来解决线程可见性问题

Volatile直解决了线程可见性问题,但没有提供原子性支持。

Volatile的使用其实还是很有难度的,要正确的使用Volatile,避免出现因没有考虑原子性问题而出现使用不当问题,所以在正在开发中Volatile使用还是较少的。

要充分的了解Volatile,要知道一下知识点

  • 指令重排序
  • 内存屏障
  • 缓存行
  • 缓存一致性问题
  • 缓存失效
  • 锁缓存

关键知识

什么是可见性

在多核机器中,有多个cpu,每个cpu有自己的缓存结构(一般是三级缓存)。对于共享变量,每个cpu中都有自己的缓存副本,其实每个cpu中的缓存也并没有达到实时一致性。

这就导致一个问题,在多线编程下,某个cpu中的线程对该共享变量进行了修改,在还没有写会内存时,由于这种修改对于其他cpu中正在使用该变量的线程是不可见的话,那导致其它cpu线程使用的是该共享变量的脏值。

所以就要解决这个问题,当一个cpu中的线程修改该共享变量,能够立刻让其它cpu的缓存失效,并且让其它线程使用更新后的值。

怎么实现可见性

每个cpu都有自己的缓存,但是所有的cpu都共享一个内存,内存对所有cpu是可见的。
利用缓存一直性协议,在某个线程更改该共享变量的值时,先将其缓存写入内存,让其它cpu知道自己的缓存的值于内存中该变量的值不一致了,也就是缓存失效了,不能再使用了。

这里有个问题,每个cpu怎么知道自己的缓存于内存中的值不一致呢?每个cpu通过嗅探技术探查在总线上传递的值信息,如果发现自己缓存中对应的内存地址的值发生了变化,那就另自己缓存编程无效,并强制重新从内存中读取该共享变量的值。

要知道一点,修改变量的值,都是要经过总线进行数据传输的,cpu嗅探总线上传输的值,通过公有的内存地址对比,就能知道自己缓存中的共享变量值是否被修改了。

Volatile是怎么支持指令非重排序的

Volatile修饰的变量,是不支持指令重排序的。
其实现原理就是内存屏障,也就是在该变量的前后建立一堵墙,在该墙的范围内,不允许进行指令重排序。
关于指令重排序问题,有一个经典的例子:DCL单利对象是否需要Volatile修饰?
在该问题中,由于指令重排序的问题,在多线程情况下,有的先线程可能会拿到半初始化的单利对象。
JVM对象创建流程,类加载,验证,解析,准备,初始化等流程,而而拿到的半初始化对象,是在准备期间产生的还未初始的对象,值都是默认值。
通过Volatile修改,禁止指令重排序,该问题就解决了。

参考资料

Volatile为什么在使用是有的需要添加填充数据

这个是Volatile使用的一个技巧,但是该技巧也是针对机器的,这要看缓存行的大小来决定要添加多少字节的填充数据。 一般缓存是以缓存行进行缓存录入的,不可能为了一个变量4个字节,值将该变量拿到缓存区,因为对于内存的结构来说,取4个字节和取一个缓存行大小数据花费的一样的。 所以一般都是一次去一个缓存行数据放入缓存中(可以了解一下局部性原理)

由于Volatile特性,要进行缓存失效通知,这里有个重点,缓存通知不是针对该共享变量,是针对该共享变量所在的缓存行,也就是要失效,整个缓存行都是失效的。这就导致一个问题,如果两个变量都在一个缓存行,那将会频繁的进行缓存刷新,但是如果变量存在不同的缓存行,那就不会出现该问题,所以进行数据填充,就是要让该变量独占一个缓存行,对其进行缓存失效等,然后从内存拿数据,对其它变量没有影响。

参考资料

总结

对于该部分的总结,我觉得自己理解还有所欠缺,自己在缓存行那一块还不是很熟悉,因为底层的内存结构有些忘记了,需要再去补补。

但是对于分页分段机制,局部性原理,缓存原理还是熟悉的,即使不知道缓存行的概念,但是如果要把数据放入缓存中,那也和页数据有关,应为局部性原理就是如此应用的,将该数据的附近地址范围内的数据放入缓存中,由局部性原理就是实现数据命中问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值