java并发编程-cpu的高速缓存


CPU缓存是CPU一个重要的组成部分,CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,这种访问速度的显著差异,导致CPU可能会花费很长时间等待数据到来或把数据写入内存,基于此,现在CPU大多数情况下读写都不会直接访问内存(CPU都没有连接到内存的管脚),取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度,提高CPU的利用率。CPU缓存一般直接跟CPU芯片集成或位于主板总线互连的独立芯片上。(现阶段的CPU缓存一般直接集成在CPU上)CPU往往需要重复处理相同的数据、重复执行相同的指令,如果这部分数据、指令CPU能在CPU缓存中找到,CPU就不需要从内存或硬盘中再读取数据、指令,从而减少了整机的响应时间。

CPU的三级缓存

一级缓存(L1 Cache)

一级缓存这个名词出现应该是在Intel公司Pentium处理器时代把缓存开始分类的时候,一级缓存(Level 1 Cache)简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存,也是历史上最早出现的CPU缓存。由于一级缓存的技术难度和制造成本最高,提高容量所带来的技术难度增加和成本增加非常大。一般来说,一级缓存可以分为一级数据缓存(Data Cache,D-Cache,L1d)和一级指令缓存(Instruction Cache,I-Cache,L1i),分别用于存放数据和指令。两者可同时被CPU访问,减少了CPU多核心、多线程争用缓存造成的冲突,提高了处理器的效能。

二级缓存(L2 Cache)

CPU未命中L1的情况下继续在L2寻求命中,L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片。内部的芯片二级缓存运行速度与主频相同,而外部的二级缓存则只有主频的一半。L2高速缓存容量也会影响CPU的性能,原则是越大越好,现在家庭用CPU容量最大的是4MB,而服务器和工作站上用CPU的L2高速缓存普遍大于4MB,有的高达8MB或者19MB。

三级缓存(L3 Cache)

三级缓存是为读取二级缓存后未命中的数据设计的—种缓存,在拥有三级缓存的CPU中,只有约5%的数据需要从内存中调用,这进一步提高了CPU的效率。L3 Cache(三级缓存),分为两种,早期的是外置,截止2012年都是内置的。而它的实际作用即是,L3缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能。降低内存延迟和提升大数据量计算能力对游戏都很有帮助。而在服务器领域增加L3缓存在性能方面仍然有显著的提升。比方具有较大L3缓存的配置利用物理内存会更有效,故它比较慢的磁盘I/O子系统可以处理更多的数据请求。具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度。

工作原理

CPU要读取一个数据时,首先从Cache中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入Cache中,可以使得以后对整块数据的读取都从Cache中进行,不必再调用内存。正是这样的读取机制使CPU读取Cache的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在Cache中,只有大约10%需要从内存读取。这大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。总的来说,CPU读取数据的顺序是先Cache后内存(结构:CPU -> cache -> memory),缓存的容量远远小于主存,因此出现缓存不命中的情况在所难免,既然缓存不能包含CPU所需要的所有数据,那么缓存的存在真的有意义吗?CPU cache是肯定有它存在的意义的,至于CPU cache有什么意义,那就要看一下它的局部性原理了:

  • 时间局部性:如果某个数据被访问,那么在不久的将来它很可能再次被访问
  • 空间局部性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问

为了保证CPU访问时有较高的命中率Cache中的内容应该按一定的算法替换,其计数器清零过程可以把一些频繁调用后再不需要的数据淘汰出Cache,提高Cache的利用率。在存在CPU三级缓存的计算机体系中,CPU与内存,缓存的关系就从单个CPU缓存演变成了三级缓存的架构了。

随着技术的不断更新,目前处理器中包含多个CPU处理器,对于多核CPU,优化操作系统任务调度算法是保证效率的关键。一般任务调度算法有全局队列调度和局部队列调度。前者是指操作系统维护一个全局的任务等待队列,当系统中有一个CPU核心空闲时,操作系统就从全局任务等待队列中选取就绪任务开始在此核心上执行。下面是Intel Core i7处理器的高速缓存的模型

缓存SRAM与内存DRAM的区别

内存的DRAM其实是SDRAM(同步动态随机储存器),是DRAM(Dynamic RAM,动态)的一种。DRAM只含一个晶体管和一个电容器,集成度非常高,可以轻松做出大容量(内存),但是因为靠电容器来储存信息,所以需要不断刷新补充电容器的电荷,否则内部的数据即会消失。充电放电之间的时间差导致了DRAM比SRAM的反应要缓慢得多。高速缓存基本上都是采用SRAM存储器,SRAM是英文Static RAM的缩写,它是一种具有静态存取功能的存储器,不需要刷新电路即能保存它内部存储的数据。但SRAM相比DRAM的复杂度就高了不止一点点,所以导致SRAM的集成度很低,也是前期CPU缓存不能集成进CPU内部也有这个原因。因此相同容量的DRAM内存可以设计为较小的体积,但是SRAM却需要很大的体积,这也是不能将缓存容量做得太大的重要原因。它的特点归纳如下:优点是节能、速度快、不需要刷新时间所以凸显其数据传输速度很快,缺点是集成度低、相同的容量体积较大、而且价格较高,只能少量用于关键性系统以提高效率。SRAM和DRAM的电路图大致如下:

在一套完整的计算机体系中,一般会包含如下存储介质,它们分别是寄存器,高速缓存,内存,硬盘。它们的容量由小到大,访问速度由高到底,成本由高到低,体积则是由小到大。因此,寄存器,高速缓存等容易集成在CPU或是主板上,但是由于其成本高,所以不会大规模使用,而内存,硬盘成本较低,但体积大,不方便集成,所以可以作为外设,安装在主板上。这些存储介质的大小和访问速度大致如下图:

缓存一致性(MESI)

缓存一致性:在多核CPU中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题。而一致性协议正是用于保证多个CPU cache之间缓存共享数据的一致。试想下面一个问题:

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

缓存行

在细说缓存一致性之前,先说一下缓存行的概念,高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块,其大小是以突发读或者突发写周期的大小为基础的。它是CPU缓存中可分配的最小存储单元,大小32字节、64字节、128字节不等,这与CPU架构有关,通常来说是64字节。当从CPU从内存中取数据到cache中时,会一次取一个cacheline大小的内存大小到cache中,然后存进相应的cacheline中。总结来说就是一句话:CPU不是按字节访问内存,而是以64字节为单位的块(chunk)拿取,称为一个缓存行(cache line)。其结构如下

工作方式:当CPU从cache中读取数据的时候,会比较地址是否相同,如果相同则检查cache line的状态,再决定该数据是否有效,无效则从主存中获取数据,或者根据一致性协议发生一次cache-to–chache的数据推送;
工作效率:当CPU能够从cache中拿到有效数据的时候,消耗几个机器周期,如果发生cache miss(缓存未命中),则会消耗几十上百个机器周期;

MESI

知道了缓存行的概念后,回到之前的问题,为了解决这个问题,CPU制造商制定了一个规则:当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效。于是,在上面的情况下,Core 1发现自己的缓存中数据已无效,Core 0将立即把自己的数据写回主存,然后Core 1重新读取该数据,这个就是多级缓存-缓存一致性(MESI)。这协议用于保证多个CPU cache之间缓存共享数据的一致性。它定义了CacheLine的四种数据状态,而CPU对cache的四种操作可能会产生不一致的状态。因此缓存控制器监听到本地操作与远程操作的时候需要对地址一致的CacheLine状态做出一定的修改,从而保证数据在多个cache之间流转的一致性。这里肯定有人疑问,既然多个缓存会出现这些问题,那为什么不能让多个CPU共用一个缓存呢?当然是效率问题,如果多个CPU使用共用一个缓存,那么在每一个时钟或者指令周期,都只能有一个cpu通过缓存对内存进行操作,其他的cpu就进入了等待。流水线就产生了冒泡,会极大浪费硬件资源。所以引入多组缓存,并引入MESI协议使他们用起来的效果像只有一组缓存一样。

缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于嗅探(snooping)协议,它的基本思想是:所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以对内存读写)。CPU缓存控制器不仅仅在内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。ESI协议是当前最主流的缓存一致性协议,在MESI协议中,每个缓存行有4个状态,可用2个bit表示,它们分别是:

状态 描述 监听
M(modify) 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。
E(exclusive) 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S(shared) 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I(invalid) 该Cache line无效。

只有当缓存行处于E或者M状态时,处理器是独占这个缓存行的。当处理器想写某个缓存行时,如果它没有独占权,它必须先发送一条”我要独占权”的请求给总线,这会通知其它处理器把它们拥有的同一缓存段的拷贝失效(如果有)。只有在获得独占权后,处理器才能开始修改数据—-并且此时这个处理器知道,这个缓存行只有一份拷贝,在我自己的缓存里,所以不会有任何冲突。反之,如果有其它处理器想读取这个缓存行(马上能知道,因为一直在嗅探总线),独占或已修改的缓存行必须先回到”共享”状态。如果是已修改的缓存行,那么还要先把内容回写到内存中。引起数据状态转换的CPU cache操作也有四种,因为所有CPU核的数据都会经过总线,因此这里其实对应的就是CPU在读取或写入数据时对总线的数据请求类型,分别如下:

操作 描述
本地读取(Local read),简称LR 当前核读本地高速缓存中的数据
本地写入(Local write),简称LW 当前核将数据写到本地高速缓存中
远端读取(Remote read),简称RR 其他核将主内存中的数据读取到高速缓存中来
远端写入(Remote write),简称RW 其他核将高速缓存中的数据写回到主存里面去

MESI示意图:

状态转换和cache操作

如上文内容所述,MESI协议中cache line数据状态有4种,引起数据状态转换的CPU cache操作也有4种,因此要理解MESI协议,就要将这16种状态转换的情况讨论清楚。初始场景:在最初的时候,所有CPU中都没有数据,某一个CPU发生读操作,此时必然发生cache miss,数据从主存中读取到当前CPU的cache,状态为E(独占,只有当前CPU有数据,且和主存一致),此时如果有其他CPU也读取数据,则状态修改为S(共享,多个CPU之间拥有相同数据,并且和主存保持一致),如果其中某一个CPU发生数据修改,那么该CPU中数据状态修改为M(拥有最新数据,和主存不一致,但是以当前CPU中的为准),其他拥有该数据的核心通过缓存控制器监听到remote write行文,然后将自己拥有的数据的cache line状态修改为I(失效,和主存中的数据被认为不一致,数据不可用应该重新获取)。

1 . Modify

场景:当前CPU中数据的状态是Modifiy,表示当前CPU中拥有最新数据,虽然主存中的数据和当前CPU中的数据不一致,但是以当前CPU中的数据为准;

  • LR:此时如果发生local read,即当前CPU读数据,直接从cache中获取数据,拥有最新数据,因此状态不变;
  • LW:直接修改本地cache数据,修改后也是当前CPU拥有最新数据,因此状态不变;
  • RR:因为本地内存中有最新数据,当本地cache控制器监听到总线上有RR发生的时,必然是其他CPU发生了读主存的操作,此时为了保证一致性,当前CPU应该将数据写回主存,而随后的RR将会使得其他CPU和当前CPU拥有共同的数据,因此状态修改为S;
  • RW:当cache控制器监听到总线发生RW,当前CPU会将数据写回主存,因为随后的RW将会导致主存的数据修改,因此状态修改成I;
2 . Exclusive

场景:当前CPU中的数据状态是exclusive,表示当前CPU独占数据(其他CPU没有数据),并且和主存的数据一致;

  • LR:直接从本地cache中直接获取数据,状态不变;
  • LW:修改本地cache中的数据,状态修改成M(因为其他CPU中并没有该数据,不存在共享问题,因此不需要通知其他CPU修改cache line的状态为I);
  • RR:本地cache中有最新数据,当cache控制器监听到总线上发生RR的时候,必然是其他CPU发生了读取主存的操作,而RR操作不会导致数据修改,因此两个CPU中的数据仍和主存中的数据一致,此时cache line状态修改为S;
  • RW:同RR,当cache控制器监听到总线发生RW,必然是其他CPU将最新数据写回到主存,此时为了保证缓存一致性,当前CPU的数据状态修改为I;
3 . Shared

场景:当前CPU中的数据状态是shared,表示当前CPU和其他CPU共享数据,且数据在多个CPU之间一致、多个CPU之间的数据和主存一致;

  • LR:直接从cache中读取数据,状态不变;
  • LW:发生本地写,并不会将数据立即写回主存,而是在稍后的一个时间再写回主存,因此为了保证缓存一致性,当前CPU的cache line状态修改为M,并通知其他拥有该数据的CPU该数据失效,其他CPU将cache line状态修改为I;
  • RR:状态不变,因为多个CPU中的数据和主存一致;
  • RW:当监听到总线发生了RW,意味着其他CPU发生了写主存操作,此时本地cache中的数据既不是最新数据,和主存也不再一致,因此当前CPU的cache line状态修改为I;
4 . Invalid

场景:当前CPU中的数据状态是invalid,表示当前CPU中是脏数据,不可用,其他CPU可能有数据、也可能没有数据;

  • LR:因为当前CPU的cache line数据不可用,因此会发生读内存,此时的情形如下。
    1. 如果其他CPU中无数据则状态修改为E;
    2. 如果其他CPU中有数据且状态为S或E则状态修改为S;
    3. 如果其他CPU中有数据且状态为M,那么其他CPU首先发生RW将M状态的数据写回主存并修改状态为S,随后当前CPU读取主存数据,也将状态修改为S;
  • LW:因为当前CPU的cache line数据无效,因此发生LW会直接操作本地cache,此时的情形如下。
    1. 如果其他CPU中无数据,则将本地cache line的状态修改为M;
    2. 如果其他CPU中有数据且状态为S或E,则修改本地cache,通知其他CPU将数据修改为I,当前CPU中的cache line状态修改为M;
    3. 如果其他CPU中有数据且状态为M,则其他CPU首先将数据写回主存,并将状态修改为I,当前CPU中的cache line转台修改为M;
  • RR:监听到总线发生RR操作,表示有其他CPU读取内存,和本地cache无关,状态不变;
  • RW:监听到总线发生RW操作,表示有其他CPU写主存,和本地cache无关,状态不变;

java内存模型与硬件内存架构的关系

学习和使用java语言的开发者,都不可避免的要回到JVM这个模型上来。然而JVM只是一个逻辑上的计算机架构模型,运行于它之上的java服务不可避免的要与底层的操作系统交互,那么JVM的内存模型与硬件的内存架构是怎么样的对应关系呢?本人在学习JVM时也常有这样的疑问,掌握了JVM的内存模型确实能帮助我们理解许多java开发方面的问题,但归根结底还是要驱动硬件工作的,下面就来探讨一下JVM与硬件上内存的关系。

JVM内存模型

为了屏蔽各种硬件和操作系统内存的访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果,所以Java虚拟机规范中定义了Java内存模型(Java Memory Model简称JMM)。Java内存模型是一种规范,它定义了Java虚拟机与计算机内存是如何协同工作的。java内存模型的主要目的是定义程序中各个变量的访问规则,以及在必须时如何同步地访问共享变量,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,JVM内存模型如下图

  • Head(堆):java里的堆是一个运行时的数据区,堆是由垃圾回收机制来负责的。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,而且Java的垃圾回收机制也会自动的收走那些不再使用的数据。但是它也有缺点,由于是运行时动态分配内存,因此它的存取速度相对要慢一些。
  • Stack(栈):栈的优势是存取速度比堆要快,仅次于计算机里的寄存器,栈的数据是可以共享的。而栈的缺点则是存在栈中的数据的大小以及生存期必须是确定的,缺乏一些灵活性,所以栈中主要用来存储一些基本数据类型和引用数据类型。

Java内存模型要求调用栈和本地变量存放在线程栈(Thread Stack)上,而对象则存放在堆上。一个本地变量也可能是指向一个对象的引用,这种情况下这个保存对象引用的本地变量是存放在线程栈上的,但是对象本身则是存放在堆上的。一个对象可能包含方法,而这些方法可能包含着本地变量,这些本地变量仍然是存放在线程栈上的。即使这些方法所属的对象是存放在堆上的。一个对象的成员变量,可能会随着所属对象而存放在堆上,不管这个成员变量是原始类型还是引用类型。静态成员变量则是随着类的定义一起存放在堆上。存放在堆上的对象,可以被持有这个对象的引用的线程访问。当一个线程可以访问某个对象时,它也可以访问该对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,那么它们都将会访问这个方法中的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

java内存模型和硬件内存架构之间的桥接

上面已经提到,Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件而言,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。在java动态的内存模型中,分为主内存,和线程工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。每个线程之间的共享变量存储在主内存里面,每个线程都有一个私有的本地内存,本地内存是Java内存模型的一个抽象的概念,并不是真实存在的。从一个更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。因此Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。主内存则可理解为物理主存的抽象。而JVM的静态内存存储模型(JVM内存模型)只是一种对物理内存的划分而已,它只局限在物理内存,而且只局限在JVM进程中的的物理内存。

如果上图中的线程A和线程B要通信,必须经历两个步骤:

  1. 首先线程A要把本地内存A中更新过的共享变量刷新到主内存里
  2. 然后线程B再到主内存中去读取线程A更新的共享变量,这样就完成了两个线程之间的通信了

因此,多线程的环境下就会出现线程安全问题。例如我们要进行一个计数的操作:线程A在主内存中读取到了变量值为1,然后保存到本地内存A中进行累加。就在此时线程B并没有等待线程A把累加后的结果写入到主内存中再进行读取,而是在主内存中直接读取到了变量值为1,然后保存到本地内存B中进行累加。此时,两个线程之间的数据是不可见的,当两个线程同时把计算后的结果都写入到主内存中,就导致了计算结果是错误的。这种情况下,我们就需要采取一些同步的手段,确保在并发环境下,程序处理结果的准确性。

多线程的三个特性

1、原子性(Atomicity)
  原子性是指一个原子操作在cpu中不可以暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。原子操作保证了原子性问题。

  x++(包含三个原子操作)a.将变量x 值取出放在寄存器中 b.将将寄存器中的值+1 c.将寄存器中的值赋值给x

由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块---synchronized关键字,因此在synchronized块之间的操作也具备原子性。

2、可见性(Visibility)

  java 内存模型的主内存和工作内存,解决了可见性问题。
  volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。

可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。

3、有序性(Ordering)
  Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

java内存模型定义同步的八种操作

JLS定义了线程对主存的操作指令:lock,unlock,read,load,use,assign,store,write。这些行为是不可分解的原子操作,在使用上相互依赖,read-load从主内存复制变量到当前工作内存,use-assign执行代码改变共享变量值,store-write用工作内存数据刷新主存相关内容。

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

java中的volatile关键字

定义为volatile类型的变量拥有两种语义:

  1. 变量的修改对所有线程可见
  • 线程中每次use变量时,都需要连续执行read->load->use几项操作,即所谓的每次使用都要从主内存更新变量值,这样其它线程的修改对该线程就是可见的。

  • 线程每次assign变量时,都需要连续执行assign->store->write几项操作,即所谓每次更新完后都会回写到主内存,这样使得其它线程读到的都是最新数据。

  1. 禁止指令重排

有了上面的理论基础,我们可以研究volatile关键字到底是如何实现的。首先写一段简单的代码:


public class LazySingleton {

    private static volatile LazySingleton instance = null;

    public static LazySingleton getInstance() {
        if (instance == null) 
            instance = new LazySingleton();
        return instance;
    }

    public static void main(String[] args) {
        LazySingleton.getInstance();
    }

}

将代码转换为汇编指令,在汇编指令的中我们可以找到如下信息

0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance
; - org.xrq.test.design.singleton.LazySingleton::getInstance@12 (line 13)

之所以定位到这两行是因为这里结尾写明了line 13,line 13即volatile变量instance赋值的地方。后面的add dword ptr [rsp],0h都是正常的汇编语句,意思是将双字节的栈指针寄存器+0,这里的关键就是add前面的lock指令,观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  • 它会强制将对缓存的修改操作立即写入主存;

  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

这里需注意虽然volatile关键字保证了变量对于线程的可见性,但并不保证线程安全。举个例子,线程1对变量进行读取操作之后,被阻塞了的话,并没有对变量值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。根源就在这里,而且volatile也无法保证对变量的任何操作都是原子性的。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
java并发编程-cpu的流水线 java并发编程-cpu的流水线
作为程序员,CPU在我们的工作中扮演了核心角色,因此了解处理器内部的工作方式对程序员来说不无裨益。CPU是如何工作的呢?一条指令执行需要多长时间?当我们讨论某个新款处理器拥有12级流水线还是18级流水线,甚至是更深的31级流水线时,这到些都
2019-03-30
Next 
聊聊运维监控 聊聊运维监控
最近在公司参与公司运维监控平台的建设,用到一些关于监控的第三方开源工具包,在此记录一下。说到运维监控,本人在公司就曾经历过一段痛苦的日子,在某个重要的日子,由于公司没有完善运维监控平台,为保障系统稳定。公司大部分的开发人员和售后全都扑在了服
2019-02-17
  TOC