Java多线程高并发进阶篇(一)-volatile实现原理剖析

我们知道,在JVM的类加载机制中,是将外围的源码文件编译成字节码文件(.class)后加载到JVM中,然后Java通过执行引擎执行字节码,最终转化为汇编指令由CPU执行.我们说的并发编程机制,当然离不开JVM的实现和CPU的指令集.

了解JMM(Java Memory Model,Java内存模型)都知道,JMM是围绕着原子性,有序性,可见性展开的.后面我会专门写一篇,阐述Java内存模型以及它与处理器内存模型,以及顺序一致性内存模型的关系.

1.volatile的定义

在Java语言规范中,对volatile的定义如下:

Java编程语言允许线程访问共享变量,为了确保共享变量能准确的访问一致性的更新,线程应该确保通过排它锁单独获得这个变量,因此Java语言提供了volatile.如果一个变量被定义为volatile,那么Java线程的内存模型(也叫线程的本地内存)看到这个变量的内容是一致的.换句话说,Java语言提供volatile,是为了保证线程之间共享变量的可见性.

2.volatile的实现原理

在了解volatile之前,我们需要明白CPU中的一些专业术语.


 

在了解了上面的专业术语后,我们就了解下volatile如何保证可见性.

有volatile修饰的共享变量在进行写操作时,转变成汇编指令时,会多出一行lock addl $0×0,(%esp);

lock前缀指令在多核处理器下会引发两件事情:

①将当前处理器缓存行的数据全部写回到系统内存(JMM中的主内存).

②在这个写回主内存的操作会使在其他处理器(CPU)中缓存了该内存地址的数据无效.

这里我们解释一下.

为了提高处理速度,处理器不直接和主内存进行通信,而是先将主内存的数据读取到处理器的内部缓存中(一级或二级缓存或其他).如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,如红色字体部分,然后将这个变量所在的缓存行数据写回到主内存.如果其他处理器缓存的该共享变量的值还是旧的,那么后续的操作就会有问题.因此,在多处理器情况下,为了保证各个处理器缓存该共享变量的内容一致,就需要根据缓存一致性协议,每个处理器通过嗅探总线上传播的数据来检查自己的缓存行是否过期了,如果发现过期了,就会把自身存储该共享变量的缓存行置为无效状态,当处理器需要对这个数据进行修改操作时,那么就需要去主内存重新读取这个共享变量的值,此时当然读取到的是上一个处理器修改过的内容了.

3.关于volatile实现原理两条原则的详述

在2中,我们说到了lock指令在多处理器情况下引发的两件事情.我们来具体介绍下这两件事情.

①lock指令会引起理器存回写到内存

旧型号的处理器中,Lock前缀指令执行期间,会声言一个LOCK#信号,该信号确保处理器在指令执行期间,可以独占共享内存(也就是主内存).当一个处理器在总线上声言LOCK#信号时,其他处理器的请求将会被阻塞.这种方式叫"总线锁"

但是在最新型号的处理器中,不再使用LOCK#信号锁总线,而是锁缓,因为锁总线的开销太大了.如果处理器访问的内容已经缓存在处理器内部,它会锁定该内容所在的内存区域的缓存行,并将该缓存行内容写回到共享内存(也就是主内存),并使用缓存一致性协议保证修改的原子性,这个过程叫做"缓存锁定".

这里要说明一点:存一致性机制会阻止同修改由两个以上理器存的内存区域数据。

一个理器的存回写到主内存会致其他理器的存无效

在处理器中,使用MESI(修改,独占,共享,无效)控制协议 ,去维护处理器内部的缓存和其他处理器的缓存一致性.在多核处理器系统中,处理器能嗅探到其他处理器访问主内存以及它们的内部缓存.处理器使用嗅探技术保证它的内部缓存,主内存,其他处理器的缓存的数据跟总线上保持一致.

比如,PentiumP6 family理器中,如果一个器通过嗅探检测到其他理器打算将共享变量回写到主内存地址,那么个地址对于缓存了该数据缓存行的处理器来说于共享状态,这个处理器就会把自身处理器缓存中对应的缓存数据行置为无效状态.

来一张图,让蒙圈的小伙伴们醒醒.JMM的内存结构示意图.



 

猜你喜欢

转载自zhaodengfeng1989.iteye.com/blog/2418346