Java高并发(四):volatile

volatile

  • volatile主要是为了保证在多处理器下,共享变量的可见性。
  • 可见性在单线程的环境下不会出现问题;但是对于多线程,读写位于不同的线程中时,可能会发生线程读取到的数据不是其他线程写入的最新的值。
  • 然而volatile可以解决线程的可见性带来的问题

volatile如何保证可见性

  • 当成员变量被volatile修饰时,在汇编代码中会加上一个lock指令,lock汇编指令可以基于总线锁或者缓存锁的机制来达到可见性;

可见性本质

硬件层面

  • 一台计算机中最核心的组件是CPU、内存和I/O设备,从处理速度来看,CPU的计算速度是最快的,其次是内存,最后是I/O设备比如磁盘。在绝大部分的程序中,一定会对内存访问,有时还会对I/O设备访问。虽然现在CPU已经从单核升级到多核,并且使用了超线程技术最大化地提升CPU的处理性能,但是对于内存和I/O的性能没有提高的话,根据木桶原理,整体的性能还是不会太高。为了平衡三者的速度差异,最大化地利用CPU处理性能,从硬件、操作系统、编译器等方面都做了优化:
    1、CPU增加了高速缓存
    2、操作系统增加了线程、进程,通过CPU的时间片切换最大化地提升了CPU的利用率
    3、编译器指令优化,更合理的利用好CPU高速缓存,
CPU高速缓存
  • 线程是CPU调度的最小单元,线程设计的目的是更充分的利用计算机的处理性能,并且在大部分场景下,需要与内存交互,并访问I/O设备。由于计算机的存储设备和寄存器的运算速度差异很大,所以计算机系统增加了一层高速缓存作为处理器和内存之间的缓冲,高速缓存的速度比较接近处理器的处理速度:把需要处理的数据复制到高速缓存中,使得数据的运算十分迅速,当运算完毕,再回写同步到内存中。
  • 在这里插入图片描述
  • 通过高速缓存的存储交互比较好的解决了处理器和内存之间的速度矛盾,但是提高了计算机系统的复杂度,同时带来了一个新的问题,那就是缓存一致性
缓存一致性
  • 借助高速缓存,CPU处理过程是:将需要用到的数据缓存到CPU高速缓存中,在计算时直接从高速缓存中读取并计算,之后写入缓存中,整个过程完成以后,再同步到主内存。
  • 在多核CPU中,由于每个线程可能运行在不同的CPU中,并且每个线程拥有自己的高速缓存。这样,同一个数据就会被缓存到多个CPU中,那么在不同的CPU中所运行的不同线程所看到的数据缓存值就会不一致,这就是缓存一致性的问题;为了解决这一问题,CPU主要提供了两种解决办法:
总线锁和缓存锁
  • 总线锁就是在多核CPU的环境下,当其中一个处理器对共享内存操作的时候,会通过总线在CPU和内存之间的通信加上锁,使得其他处理器无法通过总线来访问共享数据,锁定期间,其他处理器就不能操作其他内存地址的数据,产生阻塞,所以性能开销比较大,这种方式是不太合适的
  • 为了优化性能并降低锁的粒度,引入了缓存锁,目的就是保证多个CPU所缓存的同一份数据是一致的就可以了,缓存锁的核心机制是基于缓存一致性协议实现的

缓存一致性协议

  • 最常见的就是MESI缓存一致性协议,MESI表示的是缓存的四种状态:
1、M(Modify)
  • 表示共享数据只缓存在当前CPU中,并且是修改状态,此时缓存的数据和主内存数据不一致
2、E(Exclusive)
  • 表示缓存的独占状态,数据只缓存到当前CPU中,并且没有做修改
3、S(Shared)
  • 表示数据可能被多个CPU缓存,并且缓存数据和主内存是一致的
4、I(Invalid)
  • 表示缓存已经失效
MESI协议下CPU操作原则
  • 每个缓存的缓存控制器可以监听到其他控制器的操作,对于CPU的操作来说会遵循以下原则:
  • 1、CPU读:可以读取状态为MES的数据,I状态的数据需要从主内存读取
  • 2、CPU写:状态为ME的数据可以写,对于S状态的数据,需要将其他CPU的缓存置为无效才可以写
    在这里插入图片描述
    可见性的本质就是如果多个CPU同时缓存了相同的数据时,可能存在可见性问题,也就是CPU0修改了本地的缓存数据而对CPU1不可见,导致CPU1对数据写入时使用的是脏数据,造成了最终的结果不可预测。
MESI存在的问题
  • 各个CPU缓存行的状态是通过消息传递来进行的,如果CPU0要修改共享数据,就需要发送一个失效的消息给其他缓存了该数据的CPU,并且要等待接收到他们的回执,等待的这段时间,CPU0会处于阻塞状态,为了避免阻塞带来的CPU资源浪费,又引入了Store Bufferes,
  • CPU0把共享数据写入到Store Bufferes中,并发出invalid消息即可,然后CPU0就可以继续做其他的事。当有CPU就收到invalid消息,就会从Store Bufferes中取出数据,计算处理完毕再同步到主内存。
    在这里插入图片描述
  • 但是这种操作也会带来问题:1、数据提交时机不确定;2、重排序
内存屏障
  • 内存屏障就是把store buffers的指令写进内存,从而保证其他访问同一内存数据的可见性。
  • Store Memory Barrier(写屏障) 告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的
  • Load Memory Barrier(读屏障) 处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
  • Full Memory Barrier(全屏障) 确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作
    volatile关键字会生成一个 Lock 的汇编指令,这个指令其实就相当于实现了一种内存屏障
  • 通过内存屏障(memory barrier)禁止重排序,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于 volatile,编译器将在 volatile 字段的读写操作前后各插入一些内存屏障。保证了数据的可见性,并禁止了指令的重排序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值