本文转载自Java volatile 关键字底层实现原理解析

导语

在Java多线程并发编程中,volatile关键词扮演着重要角色,它是轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”。“可见性”的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。与synchronized不同,volatile变量不会引起线程上下文的切换和调度,在适合的场景下拥有更低的执行成本和更高的效率。

本文将从硬件层面详细解读volatile关键字如何保证变量在多线程之间的可见性,在此之前,有必要讲解一下CPU缓存的相关知识。

CPU缓存

CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多,举个例子:

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

这种访问速度的显著差异,导致CPU可能会花费很长时间等待数据到来或把数据写入内存。

基于此,现在CPU大多数情况下读写都不会直接访问内存(CPU都没有连接到内存的管脚),取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度。

按照读取顺序与CPU结合的紧密程度,CPU缓存可分为:

  • 一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存。
  • 二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半。
  • 三级缓存:简称L3 Cache,部分高端CPU才有。

每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增。

CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。一般来说每级缓存的命中率大概都有80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。

使用CPU缓存带来的问题

用一张图表示一下 CPU –> CPU缓存 –> 主内存 数据读取之间的关系:

图片来自网络

当系统运行时,CPU执行计算的过程如下:

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

如果服务器是单核CPU,那么这些步骤不会有任何的问题,但是如果服务器是多核CPU,那么问题来了,以Intel Core i7处理器的高速缓存概念模型为例(图片来自《深入理解计算机系统》):

试想下面一种情况:

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

为了解决这一问题,CPU制造商规定了一个缓存一致性协议

缓存一致性协议

每个CPU都有一级缓存,但是,我们却无法保证每个CPU的一级缓存数据都是一样的。 所以同一个程序,CPU进行切换的时候,切换前和切换后的数据可能会有不一致的情况。那么这个就是一个很大的问题了。 如何保证各个CPU缓存中的数据是一致的。就是CPU的缓存一致性问题。

总线锁

一种处理一致性问题的办法是使用Bus Locking(总线锁)。当一个CPU对其缓存中的数据进行操作的时候,往总线中发送一个Lock信号。 这个时候,所有CPU收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的CPU就去内存中获取最新数据更新。

但是用锁的方式总是避不开性能问题。总线锁总是会导致CPU的性能下降。所以出现另外一种维护CPU缓存一致性的方式,MESI。

MESI

MESI是保持一致性的协议。它的方法是在CPU缓存中保存一个标记位,这个标记位有四种状态:

  • M: Modify,修改缓存,当前CPU的缓存已经被修改了,即与内存中数据已经不一致了;
  • E: Exclusive,独占缓存,当前CPU的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据;
  • S: Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段;
  • I: Invalid,失效缓存,这个说明CPU中的缓存已经不能使用了。

CPU的读取遵循下面几点:

  • 如果缓存状态是I,那么就从内存中读取,否则就从缓存中直接读取。
  • 如果缓存处于M或E的CPU读取到其他CPU有读操作,就把自己的缓存写入到内存中,并将自己的状态设置为S。
  • 只有缓存状态是M或E的时候,CPU才可以修改缓存中的数据,修改后,缓存状态变为M。

这样,每个CPU都遵循上面的方式则CPU的效率就提高上来了。

volatile保证可见性的底层原理

在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作,CPU会做什么事情。

Java代码如下:

instance = new Singleton(); //instance是volatile变量

转变成汇编代码如下:

0x01a3de1d: movb $0X0,0X1104800(%esi);
0x01a3de24: lock addl $0X0,(%esp);

有volatile修饰的共享变量进行写操作时会多出第二行汇编代码,该句代码的意思是对原值加零,其中相加指令addl前有lock修饰。通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发两件事情:

1、将当前处理器缓存行的数据写回到系统内存。

Lock前缀指令导致在执行指令期间,声言处理器的LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占任何共享内存(因为它会锁住总线,导致其他CPU不能访问总线,也就不能访问系统内存,在Intel486和Pentium处理器中都是这种策略)。但是,在最近的处理器里,LOCK# 信号一般不锁总线,而是锁缓存,因为锁总线开销的比较大。在P6和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK# 信号。相反,它会锁定这块区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上的处理器缓存的内存区域数据。

2、这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

IA-32处理器和Intel 64处理器使用MESI控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强行执行缓存行填充。

Java volatile 关键字底层实现原理解析的更多相关文章

  1. Java并发之底层实现原理学习笔记

    本篇博文将介绍java并发底层的实现原理,我们知道java实现的并发操作最后肯定是由我们的CPU完成的,中间经历了将java源码编译成.class文件,然后进行加载,然后虚拟机执行引擎进行执行,解释为 ...

  2. java volatile 关键字(转)

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...

  3. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

  4. 13、Java并发性和多线程-Java Volatile关键字

    以下内容转自http://tutorials.jenkov.com/java-concurrency/volatile.html(使用谷歌翻译): Java volatile关键字用于将Java变量标 ...

  5. Java Volatile关键字(转)

    出处:  Java Volatile关键字 Java的volatile关键字用于标记一个变量“应当存储在主存”.更确切地说,每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取.每次 ...

  6. Java AOP的底层实现原理

    Java AOP的底层实现原理 一.什么是AOP 1.AOP:Aspect Oriented Programming(面向切面编程),OOP是面向对象编程,AOP是在OOP基础之上的一种更高级的设计思 ...

  7. Java volatile关键字详解

    Java volatile关键字详解 volatile是java中的一个关键字,用于修饰变量.被此关键修饰的变量可以禁止对此变量操作的指令进行重排,还有保持内存的可见性. 简言之它的作用就是: 禁止指 ...

  8. 从根源上解析 Java volatile 关键字的实现

    1.解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析Volatile关键字 使用volatile关键字的场景 2.内存模型的相关概念 缓存一致性问题.通常称这种被多个线程 ...

  9. Java volatile关键字实现原理

    场景引入 可见性问题 先来看一张图: 上面的图,是简化版的Java内存模型,一个线程有自己的工作内存,同时还有一个共享的主内存. 线程1和线程2读取数据data时,先从主内存里加载data变量的值到工 ...

随机推荐

  1. java 文件转成pdf文件 预览

    一.前端代码 //预览功能 preview: function () { //判断选中状态 var ids =""; var num = 0; $(".checkbox& ...

  2. Fastjson使用实例

    Fastjson使用实例 一.FastJson使用范例 1.1FastJson三个核心类 1.2Maven依赖 1.3Scala API 1.3.1反序列化 1.3.2序列化 1.4Java API ...

  3. 18.RAID介绍和部署磁盘阵列

    1.RAID RAID(Redundant Array of Independent Disks,独立冗余磁盘阵列)技术具备的冗余备份机制以及提升了的硬盘吞吐量. 1)RAID 0:把多块物理硬盘设备 ...

  4. Java 复习整理day03

    变量或者是常量, 只能用来存储一个数据, 例如: 存储一个整数, 小数或者字符串等. 如果需要同时存储多个同类型的数据, 用变量或者常量来实现的话, 非常的繁琐. 针对于 这种情况, 我们就可以通过数 ...

  5. Java复习整理 day01

    练习代码: 1 //这条语句说明这个Java文件在demo的包下 2 package demo1; 3 /** 4 * 5 * @author 王兴平 6 * 这个是第一个hello world 案例 ...

  6. HTML学习速记

    HTML结构导图 先看HTML结构示意图 本文主要讲解HTML当中的一些html标签,css样式,javascript方法 html html的标签有各式各样的数不胜数,但是常用的并不多,总结下我目前 ...

  7. priority_queue()大根堆和小根堆(二叉堆)

    #include<iostream> #include <queue> using namespace std; int main() { //对于基础类型 默认是大顶堆 pr ...

  8. Second My Problem First HDU - 3706 单调队列

    单调队列 单调队列是指一个队列内部的元素具有严格单调性的一种数据结构,分为单调递增队列和单调递减队列. 单调队列满足两个性质 1.单调队列必须满足从队头到队尾的严格单调性. 2.排在队列前面的比排在队 ...

  9. Codeforces Round #649 (Div. 2)

    Codeforces Round #649 (Div. 2) -- WKL \(\mathcal{A}\)题: \(\mathrm{XXXXX}\) Greedy implementation *12 ...

  10. Network UVA - 315 无向图找割点

    题意: 给你一个无向图,你需要找出来其中有几个割点 割点/割项: 1.u不为搜索起点,low[v]>=dfn[u] 2.u为搜索起点,size[ch]>=2 3.一般情况下,不建议在tar ...