Java并发与锁设计实现详述(9)- 关键字volatile底层原理

我们都知道volatile关键字是用来实现变量在多线程之间的可见性的,它是java.lang.concurrnt包的核心。

在这篇文章中将简单描述它是如何保证变量在多线程之间的可见性的。在此之前,可能需要先要了解一点CPU缓存的相关知识,从而保证我们更好的准确的使用volatile关键字。

CPU缓存

首先来看看CPU缓存,CPU缓存的出现主要是用来解决CPU运算速度和内存读取速度的不匹配问题,因为CPU运算速度比内存读取速度要快的多。

  • 一次主内存的访问通常在几十到几百个时钟周期
  • 一次L1高速缓存的读写只需要1~2个时钟周期
  • 一次L2高速缓存的读写也只需要数十个时钟周期

这种速度上的差异,直接导致CPU需要等待较长时间来读取或者写入内存中的数据。正因为如此,CPU都不是直接访问内存区域,而是与CPU缓存,CPU缓存是CPU与内存之间的临时存储器,存储容量比内存小,但是访问速度却比内存快的多。

CPU缓存按照读取的顺序,分为几个层级,分别如下:

一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存;

二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半;

三级缓存:简称L3 Cache,部分高端CPU才有;

当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。

CPU缓存带来的问题

当计算机运行时,执行顺序大致如下:

(1)程序和数据被加载到主内存;

(2)部分程序和数据并加载到CPU缓存;

(3)CPU执行指令,并将可能的数据结果更新到CPU缓存中;

(4)CPU高速缓存中的数据写回到主内存中;

对于单核CPU而言,上述流程可能不会带来什么问题,但是如果计算机是多核的,那么可能就会存在问题了。


对于上面这种i7等存在多核的处理器来说,不同核之间操作数据可能存在不同步,如下操作:

  • 核0读取了一个字节,根据局部性原理,它相邻的字节同样被被读
  • 入核0的缓存核3做了上面同样的工作,这样核0与核3的缓存拥有同样的数据
  • 核0修改了那个字节,被修改后,那个字节被写回核0的缓存,但是该信息并没有写回主存
  • 核3访问该字节,由于核0并未将数据写回主存,数据不同步

为了解决这个问题,CPU制造商制定了一个规则:当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效。于是,在上面的情况下,核3发现自己的缓存中数据已无效,核0将立即把自己的数据写回主存,然后核3重新读取该数据。

针对volatile关键字,底层都做了什么?

Java代码: instance = new Singleton();//instance是volatile变量
汇编代码: 0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
在操作volatile关键字修饰的变量时,会对变量进行加lock处理,通过查IA-32架构软件开发者手册可知,这里的lock只要起到一下效果:

(1)锁总线,使得CPU对内存的读写操作都会被堵塞,直到锁被释放;但是这种锁总线的方式很明显影响处理的效率,因此后面逐渐采用了锁缓存来替代锁总线。

(2)lock之后的写操作会会写内存已经修改的数据,并且让其他核缓存行失效,从而触发再次从内存中加载最新数据到CPU缓存;

(3)不是内存屏障,但是却起到了内存屏障的功能;

从上面可知,lock会回写内存,但是之前其他线程的缓存行却不知道缓存已经失效,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到CPU缓存里。

正因为上面几点的保证,才使得在任一时间一个线程对volatile变量的修改可以对其他线程可见。


猜你喜欢

转载自blog.csdn.net/andamajing/article/details/79718383