java volatile为什么能保证内存可见性

cpu缓存一致性:

每条指令都是在CPU核中执行的,在执行过程中势必会涉及到数据的读写。而程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,所以就有了CPU高速缓存。CPU高速缓存(L1,2)为某个CPU独有,只与在该CPU上运行的线程有关。
那么如何保证多个cpu核对于同一份内存数据的缓存一致性?
第一种,操作系统在总线上发出一个LOCK#信号,其他处理器就不能操作缓存了该共享变量内存地址的缓存,阻塞了其他CPU,使该处理器可以独享此共享内存。但总线锁定把CPU和内存的通信给锁住了,使得在锁定期间,其他处理器不能操作其他内存地址的数据,开销较大
第二种缓存一致性机制,整体来说,是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取

但是第一种总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。
第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。比锁总线效率高
MESI协议
MESI 协议是以缓存行(CPU缓存的基本数据单位)的几个状态来命名的(Modified、Exclusive、 Share or Invalid)。该协议要求在每个缓存行上维护两个状态位,使每个数据单位处于M、E、S和I这四种状态之一,各种状态含义如下:

M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。其状态相对于内存中的值来说,已经被修改,且没有更新到内存中。

E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。

S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。

I:无效的,非法的。本CPU中的这份缓存无效。

MESI,只是缓存一致性协议中的一个,到底怎么实现,还是得看具体的处理器指令集。
一般对 MESI 的简单实现都是没有实际价值,因为发生写操作往往会带来很长时间的等候:首先需要写的 CPU 需要让别的 CPU 将状态转换到 invalid状态,收到 响应以后才能进行实际的写,这个过程是阻塞的,为此在CPU的L1缓存上又设置了一层 : store buffer/load buffer(读写缓冲区)

CPU执行load读数据时,把读请求放到LoadBuffer,这样就不用等待其它CPU响应,先进行下面操作,稍后再处理这个读请求的结果。

CPU执行store写数据时,把数据写到StoreBuffer中,接着执行下面的指令。待到某个适合的时间点,把StoreBuffer的数据刷到主存中(可以发现为了效率这就出现了重排序,下面的指令先执行了)

MESI协议约定的缓存上对应的监听 四种状态是如何转化的:

  1. 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回主存。
  2. 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者修改该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
  3. 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
  4. 当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果(老子要读数据,自己主动点,有脏数据的赶紧刷到内存让我好读取),如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
  5. 当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则(也就是S状态的情况下)需要发出RFO指令(总线事务),通知其他CPU置缓存无效(I),这种情况下会性能开销是相对较大的。在写入完成后,修改其缓存状态为M。

所以如果一个变量在某段时间只被一个线程频繁地修改,则使用其内部缓存就完全可以办到,不涉及到总线事务(多个cpu只能有一个通过总线去读写主存)。如果某个地址的缓存行一会被这个CPU独占、一会被那个CPU 独占,因为第2条性质的原因,会触发 不停的调度到其他cpu失效掉它的缓存行,这时会不断产生RFO指令影响到并发性能。

协议的实现方式
A. write through:每次CPU修改了cache中的内容,立即更新到内存,也就意味着每次CPU写共享数据,都会导致总线事务,因此这种方式常常会引起总线事务的竞争,高一致性,但是效率非常低;

B.write back:每次CPU修改了cache中的数据,不会立即更新到内存,而是等到cache line在某一个必须或合适的时机才会更新到内存中;

无论cpu采取哪种方式,是写通还是写回,在多线程环境下都需要处理缓存cache一致性问题。为了保证缓存一致性,处理器又提供了写失效(write invalidate)和写更新(write update)两个操作来保证cache一致性。

写失效:当一个CPU修改了数据,如果其他CPU有该数据,则通知其为无效;

写更新:当一个CPU修改了数据,如果其他CPU有该数据,则通知其跟新数据;

写更新会导致大量的更新操作,因此在MESI协议中,采取的是写失效(即MESI中的I:ivalid,如果采用的是写更新,那么就不是MESI协议了,而是MESU协议)

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

上面说到读写缓冲,具体store buffer 的作用是让 CPU 需要写的时候仅仅将其操作交给 store buffer,然后继续执行下去,store buffer 在某个时刻就会完成一系列的同步行为;很明显这会重排序违背顺序一致性,因为如果某个 CPU 试图写其他 CPU 占有的内存,消息交给 store buffer 后,CPU 继续执行后面的指令,而如果后面的指令依赖于前面这个被写入的内存(而前面的又尚未更新到主存,这个时候读取的值是错误的)就会产生问题。所以实际实现 store buffer 会增加 snoop 特性,即 CPU 读取数据时会从 store buffer 和 cache 去读

可即便增加了 snoop,store buffer 仍然会违背 内存有序性,JMM的解决方案是 插入内存屏障(memory barrier):我们知道两个写操作,隐含的假定是如果能观察到后一个写的结果,那么前一个写的结果势必也会发生,这是一个符合人直觉的行为,但是由于 store buffer 的存在,这个结论可能并不正确

硬件 很难揣度软件上这种前后依赖关系,因而只有通过软件的手段表示(对应也需要硬件提供某种指令来支持这种语义),这个就是 memory barrier,从硬件上来看这个 barrier 就是 CPU flush 其 store buffer 的指令,那么一种做法就是提供给程序员对应的指令(封装到函数里面)要求在合适的时候插入这种关系,另一种做法就是通过某种标识让编译器翻译代码的时候自动的插入这个指令
往往 store buffer 都很小,开始写之前首先需要 invalidate 其他cpu cache 里面的数据,为了加速这个过程硬件设计者又加入了 invalidate queue,这个 queue 将 incoming 的 invalidate 消息存放,立即返回对应的 response(我的缓存行状态改完了,你继续操作) 这样以便发起者能尽快做后面的事情,而这个 CPU 可以通过 invalidate queue 后续处理这些内存
invalidate queue 的存在会使得有更多的地方需要 memory barrier

状态更新举例:
状态转换和cache操作
如上文内容所述,MESI协议中cache line数据状态有4种,引起数据状态转换的CPU cache操作也有4种 LR(local read) LW(local write) RR(remote read) RW(read write)

在初始的时候,所有CPU中都没有数据,某一个CPU(也就是线程)发生读操作,此时必然发生cache miss,数据从主存中读取到当前CPU的cache,状态为E(独占,只有当前CPU有数据,且和主存一致),此时如果有其他CPU也读取数据,则状态修改为S(共享,多个CPU之间拥有相同数据,并且和主存保持一致),如果其中某一个CPU发生数据修改,那么该CPU中数据状态修改为M(拥有最新数据,和主存不一致,但是以当前CPU中的为准),其他拥有该数据的核心通过缓存控制器监听到remote write,然后将自己拥有的数据的cache line状态修改为I(失效,和主存中的数据被认为不一致,数据不可用应该重新从主存中获取)。

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

LR:此时如果发生local read,即当前CPU读数据,直接从cache中获取数据,拥有最新数据,因此状态不变;

LW:直接修改本地cache数据,修改后也是当前CPU拥有最新数据,因此状态不变;

RR:因为本地内存中有最新数据,当本地cache控制器监听到总线上有RR发生的时,必然是其他CPU发生了读主存的操作,此时为了保证一致性,当前CPU应该将数据写回主存,而随后的RR将会使得其他CPU和当前CPU拥有共同的数据,因此状态修改为S;

RW:同RR,当cache控制器监听到总线发生RW,当前CPU会将数据写回主存,因为随后的RW将会导致主存的数据修改,因此状态修改成I;

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;

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;

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

LR:因为当前CPU的cache line数据不可用,因此会发生读内存,此时的情形如下。

    A. 如果其他CPU中无数据则状态修改为E;

    B. 如果其他CPU中有数据且状态为S或E则状态修改为S;

    C. 如果其他CPU中有数据且状态为M,那么其他CPU首先发生RW将M状态的数据写回主存并修改状态为S,随后当前CPU读取主存数据,也将状态修改为S;

LW:因为当前CPU的cache line数据无效,因此发生LW会直接操作本地cache,此时的情形如下。

    A. 如果其他CPU中无数据,则将本地cache line的状态修改为M;

    B. 如果其他CPU中有数据且状态为S或E,则修改本地cache,通知其他CPU将数据修改为I,当前CPU中的cache line状态修改为M;

    C. 如果其他CPU中有数据且状态为M,则其他CPU首先将数据写回主存,并将状态修改为I,当前CPU中的cache line转台修改为M;

RR:监听到总线发生RR操作,表示有其他CPU读取内存,和本地cache无关,状态不变;

RW:监听到总线发生RW操作,表示有其他CPU写主存,和本地cache无关,状态不变;

重排序

上面说到多个cpu核是如何保证缓存的一致性的,由于cpu在执行程序时为了提高性能,使用到了load/storebuffer,会导致cpu的执行顺序与程序不一致,因为发生了指令的重排–处理器会把没有依赖的两个操作进行重排序,使后面的操作可以先于前面的操作执行。

所有处理器内存模型都允许无依赖写-读(SL)重排序,原因:它们都使用了写缓存区,写缓存区可能导致 写-读 操作重排序。
越是追求性能的处理器,内存模型设计的会越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。而且处理器内存模型比顺序一致性内存模型要弱。

由于常见的处理器内存模型比JMM要弱,java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱并不相同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不相同

这里所说的依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑,所以多线程的问题也随之而来

class Example {
	int a = 0;
	boolean flag = false;
	
	public void writer() {
	    a = 1;                   //1
	    flag = true;           //2
	}
	
	Public void reader() {
	    if (flag) {                //3
	        int i =  a + a;       //4
	    }
	}
}

假如某个cpu对操作1和操作2做了重排序(因为这两步不存在依赖关系,单线程环境不会改变最终结果)。程序执行时,线程A首先写变量flag到内存(如果线程A没写完,也就是还没刷到主存,此时不会发生问题。java为了让读线程一定能看到最终的结果的话,就要用volatile修饰flag,至于为什么后面happensbefore volatile变量规则会解释),随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还根本没有被线程A写入内存,多线程程序的语义被重排序破坏了
as-if-serial
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序提供:单线程程序是按程序的顺序来执行的。无需担心重排序会干扰程序的运行结果,也无需担心内存可见性问题
控制依赖
操作3和操作4存在控制依赖关系(条件关系)。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。执行线程B的处理器可以先提前读取并计算a+a,然后把计算结果临时保存到一个名为重排序缓冲的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。

可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义。最终程序的结果就出乎意料了

JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内存模型在设计时会对顺序一致性模型做一些放松,如果完全按照顺序一致性模型来实现处理器和JMM,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很大的影响。

又一个例子:
Processor A   a = 1; //A1
                      x = b; //A2

Processor B   b = 2; //B1
                      y = a; //B2

初始状态:a = b = 0
有可能得到结果:x = y = 0

处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1StoreBuffer,B1StoreBuffer),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的数据刷新到内存中。当以这种时序执行时,可以得到x = y = 0的结果。
从内存操作实际发生的顺序来看,直到处理器A刷新自己的写缓存到内存时,写操作A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器A的内存操作顺序被重排序了。
由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。现代的处理器都会允许对写-读操作重排序(因为写要等,为了避免等待,可以先将无依赖的读操作完成)。

重排序三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序(所有如果程序是单线程程序,不用担心这种问题)。

  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,就应该充分利用硬件部件,避免空闲。处理器可以改变语句对应机器指令的执行顺序。但对于一串给定的指令,处理器会找出非真正数据依赖的指令,让他们并行执行。对于读取指令执行结果在写回到寄存器的时候,必须是顺序的。也就是说,哪怕是先被执行的读取指令,它的运算结果也是按照指令次序写回到最终的寄存器的

  3. 内存系统的重排序。由于处理器使用缓存(L1,2,3)和读/写缓冲区(storebuffer loadbuffer),这使得读取和写入操作看上去可能是在乱序执行。

因此在设计JMM时,需要考虑两个关键因素:
易于编程,基于一个强内存模型来编写代码。
编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能,即编译器和处理器希望实现一个弱内存模型。
由于这两个因素互相矛盾,设计JMM时的核心目标就是找到一个好的平衡点:一方面要提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能的放松

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JMM把内存屏障指令分为下列四类:

  1. LoadLoad Barriers Load1; LoadLoad; Load2 确保Load1数据的装载,hb Load2及所有后续装载指令的装载。
  2. StoreStore Barriers Store1; StoreStore; Store2 确保Store1数据对其他处理器可见(刷新到内存),hb Store2及所有后续存储指令的存储。
  3. LoadStore Barriers Load1; LoadStore; Store2 确保Load1数据装载,hb Store2及所有后续的存储指令刷新到内存。
  4. StoreLoad Barriers Store1; StoreLoad; Load2 确保Store1数据对其他处理器变得可见(保证数据刷新到内存),hb Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。
    StoreLoad Barriers同时具有其他三个屏障的效果。所以现代的多处理器大都支持该屏障。执行该屏障会有很大开销,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中
    可以想象 如果在 A1 与 A2 A3与A4插入一条 SL屏障即可解决上述问题

顺序一致性
顺序一致性内存模型为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

一个线程中的所有操作必须按照程序的顺序来执行。
(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

happens-before原则(可认为重排序准则,指导编程的准则)

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间

也就是如果两个操作存在 HB关系(满足下面的任意一条规则) 则前一个操作(执行的结果)必然对后一个操作可见,也就是不可被重排序,指令执行从上到下一条一条来

1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

程序次序规则:一段代码在单线程中执行的结果是有序的。因为虚拟机、处理器会对指令进行重排序。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。这个规则只对单线程有效,在多线程环境下无法保证正确性。

锁定规则:无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。

volatile变量规则它标志着volatile保证了线程可见性。如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的·。

传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C

线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。

线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。
其他满足happens-before的规则:
将一个元素放入一个线程安全容器的操作hb从容器中取出这个元素的操作
在CountDownLatch上的减数操作hb CountDownLatch#await()操作
释放Semaphore许可的操作hb获得许可操作
Future表示的任务的所有操作hb Future#get()操作
向Executor提交一个Runnable或Callable的操作hb任务开始执行操作

如果两个操作不存在上述任一一个happens-before规则,那么这两个操作就没有顺序的保障,即JVM可以对这两个操作进行重排序。**如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的**。

happens-before在底层如何实现?
上面讲到 JMM会在指令间插入内存屏障来保证顺序一致性,而
storeload屏障会触发 CPU 将屏障之前的操作缓存回写到内存,借助 CPU 缓存一致性机制,使得其它处理器核心能够看到最新的共享变量,实现了共享变量对于所有 CPU 的可见性。

当一个CPU进行写入时,首先会给其它CPU发送Invalid消息,然后把当前写入的数据写入到Store Buffer中。然后异步在某个时刻真正的写入到Cache中,最后由缓存刷到内存。

当前CPU核如果要读Cache中的数据,需要先扫描load Buffer之后再读取Cache。

但是此时其它CPU核是看不到当前核的Store Buffer中的数据的,要等到Store Buffer中的数据被刷到了Cache之后才会触发失效操作。

而当一个CPU核收到Invalid消息时,会把消息写入自身的Invalidate Queue中,随后异步将其设为Invalid状态。

·举例·
double a= 3.14; //A
double b= 1.0; //B
double multi= a*b; //C
上面代码存在三个happens- before关系:
A hb B; 1
B hb C; 2
A hb C; 3
A hb B,从程序语义的角度来说,对A和B做重排序即不会改变程序的执行结果,也还能提高程序的执行性能。也就是说,上面这3个hb关系中,1是不必要的(不满足上面任何一条性质,jvm可以重排)。JMM把happens- before要求禁止的重排序分为了下面两类:

会改变程序执行结果的重排序。
不会改变程序执行结果的重排序。
JMM对这两种不同性质的重排序,采取了不同的策略:
对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
对于不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(JMM允许这种重排序)。

只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。比如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。如果认定一个volatile变量仅仅只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。

JMM的内存可见性保证
Java程序的内存可见性保证按程序类型可以分为下列三类:
单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。

volatile

其实对一个volatile变量的单个读/写操作,可以认为与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同
由上面happens before规则第二点锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

简而言之,volatile变量自身具有下列特性:
可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性,但注意volatile++这种复合操作不具有原子性。
总结:

Java提供了volatile来保证可见性。volatile就是一个践行happens-before的关键字
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。

Java提供volatile保证一定的有序性
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的

即volatile 保证可见性、不保证原子性 禁止指令重排序

volatile的实现方式,实际上就是限制了重排序的范围——加入内存屏障。

每个volatile写操作的前面插入一个StoreStore屏障
每个volatile写操作的后面插入一个StoreLoad屏障
每个volatile读操作的后面插入一个LoadLoad屏障
每个volatile读操作的后面插入一个LoadStore屏障

	int a = 0;
	volatile int b = 0;
	...
	a = 1; // line 1
	b = 2; // line 2
   这里line 1   一定 会 happens before  line 2 
   因为满足了条件 3 volatile变量规则 禁止了重排  
   而java在每个volatile变量写的前面line 2的前面插入SS屏障(见上面StoreStore内存屏障说明),
   由cpu缓存一致性规则通知其他cpu对应缓存状态的改变,
   然后触发cpu的回写机制 (即发布所有核心内部的写操作到内存),
   其他cpu读取volatile变量时也会在前后加入内存屏障,根据自己的共享数据的缓存状态做对应的操作
  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值