多线程并发之volatile的底层实现原理

上篇博文从内存可见性看Volatile、原子变量和CAS算法提到了volatile保证内存可见性和CAS算法。本篇博文着重学习volatile的底层实现原理。

【1】回顾volatile

volatile相当于是轻量级的synchronized。如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。

通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性(内存可见性)。

【2】计算机的CPU,主存,和高速缓存

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存(实际上是一个多级寄存器)。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。如图:
在这里插入图片描述

有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。

在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。

举一个简单的例子:i++操作。

这种操作在单CPU是没问题的,但是现在都是多核多CPU多线程的时代,那么当他们都同时进行i++操作,那么就有问题了。所以硬件厂商要想办法解决这个问题,所以有了两个解决缓存一致性的方案(即在多CPU下,如何保证原子性):

  • 通过在总线加LOCK#锁的方式
  • 通过缓存一致性协议

抽象图示如下:
在这里插入图片描述

注意:这部分内容可类比java内存模型JMM。

但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。

第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。


【3】volatile原理

加了volatile修饰符的共享变量之所以能在线程之间保证可见性,是因为该变量加了基于CPU的内存屏障指令,被JSR-133的java内存模型抽象为happens-before原则。

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。这个指令就相当于一个内存屏障。

具体表现为:

  • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
  • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

从而保证了,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。

参考博文:https://mp.weixin.qq.com/s/D6bQDmNJrXvksO0WsHla6g

猜你喜欢

转载自blog.csdn.net/J080624/article/details/85318075