内存屏障与java的内存屏障

  • 内存屏障
  • 前言
  • 一、什么是内存屏障?
  • 二、volatile变量规则
  • 1.volatile简介
  • 2.volatile原理
  • 3.volatile特性
  • 4.volatile变量规则
  • 四、内存屏障的标准
  • 硬件上面的内存屏障
  • Java的内存屏障
  • 五、X86架构的内存屏障
  • Store Barrier
  • Load Barrier
  • Full Barrier
  • 六、volatile引出的可见性和重排序问题,内存屏障是如何解决的
  • 八、CAS
  • 九、锁


内存屏障

前言

     在学习JVM的乱序问题的时候,为了现在的CPU效率的提高,会做出各种各样的优化,有个优化就叫做 CPU 乱序执行,CPU乱序执行在单线程环境下是一种很好的优化手段,但是在多线程环境下,就会出现数据不一致的问题,因此就可以通过内存屏障这个机制来处理这个问题。

     而内存屏障是CPU的,与Java的内存屏障有很大的区别,所以接下来就来解释一下内存屏障和Java内存屏障,自己也参考了很多文献,结合自己的理解来深入理解内存屏障和Java内存屏障。

一、什么是内存屏障?

     其实一开始在接触内存屏障这个词的时候,就可以从内存两个字知道这个是底层与硬件有关的机制,从字面意思屏障指的是屏风或阻挡之物,也有保护遮蔽的含义。所以说内存屏障是硬件之上、操作系统或JVM之下,对并发作出的最后一层支持。

     内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

     先通过volatile关键字的语义来引出以下几个问题:

  • 可见性
  • 重排序

     为了更好的理解内存屏障,引出内存屏障的一个基本问题:

  • 内存屏障的标准
  • volatile引出的可见性和重排序问题,内存屏障是如何解决的
  • 内存屏障上的几个封装
  • 为了深入理解,了解硬件架构的基本原理

二、volatile变量规则

1.volatile简介

     volatile是Java提供的一种轻量级的同步机制。它是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,可见性的意思是一个线程修改一个共享变量时,另一个线程可以读到这个修改的值,如果volatile使用恰当的话,它比synchronized的使用成本更低,因为它不会引起线程的上下文切换和调度。

2.volatile原理

     volatile变量修饰的共享变量进行写操作时会在汇编代码前加上lock前缀,lock前缀的指令可以在多核处理器下。

3.volatile特性

(1)可见性

     定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

线程在JMM(Java memory model)Java内存模型中的流程:

     由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为这个线程创建一个工作内存,工作内存是每个线程的私有数据区域。JMM(Java memory model)中规定所有变量都存储在主内存里,主内存是共享内存区域,所以所有线程都可以进行访问,但是线程对变量的操作(读/写)必须在自己的工作内存中进行。具体步骤:

  • 首先将变量从主内存中拷贝到自己的工作内存空间
  • 然后对变量进行操作
  • 操作完成后再将变量写回主内存

     注意:不能直接操作主内存中的变量,各个线程中的工作内存存储着的是主内存中的变量副本拷贝的。因此不同线程间无法访问对方的工作内存,线程的通信必须通过主内存来完成。

更加具体步骤,如下图所示:

java内存屏障原理 java内存屏障详解_经验分享

     在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

(2)不保持原子性

     原子性定义:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

     不保持原子性:对任意单个volatile变量的 读/写 具有原子性,但类似于volatile++这种复合操作不具有原子性。

(3)禁止指令重排序

     重排序定义:是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。

     重排序并没有严格的定义。整体上可以分为两种:

  • 真·重排序:编译器、底层硬件(CPU等)出于“优化”的目的,按照某种规则将指令重新排序(尽管有时候看起来像乱序)。
  • 伪·重排序:由于缓存同步顺序等问题,看起来指令被重排序了。

     重排序问题源自三种场景:

  • 编译器编译时的优化
  • 处理器执行时的乱序优化
  • 缓存同步顺序(导致可见性问题)

     在多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的共享变量的一致性时无法保证的,所以结果无法预测。volatile 实现了禁止指令重排序优化,从而避免了多线程环境下程序出现乱序执行的现象。

     实现:在对volatile变量进行写操作时,会在写操作后面加入一条**Memory Barriier(内存屏障)**告诉内存和CPU,禁止在内存屏障前后的执行指令重排优化。

volatile的底层内存屏障:

java内存屏障原理 java内存屏障详解_java内存屏障原理_02

volatile的底层内存屏障:

java内存屏障原理 java内存屏障详解_开发语言_03

4.volatile变量规则

     volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行

     把对volatile变量的单个 读/写 看成是使用同一个锁对这些单个 读/写 操作做了同步。从JDK1.5开始,volatile变量的 写-读 可以实现线程间的通信。从内存语意上来讲,volatile的 写-读 与锁的 释放-获取 有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取具有相同的内存语义。

volatile的内存语义

  • volatile写的内存语义:当写一个volatile变量时,JMM(Java内存模型)会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile内存语义的实现

     为了实现volatile内存语义,JMM会分别限制两种类型的重排序:编译重排序和处理器重排序。

     如下JMM针对编译器制定的volatile重排序规则:

     当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

     当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

     当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

     基于保守策略的JMM内存屏障插入策略:为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

如:Intel 的内存屏障的设计比较简单,只有三条指令:

  • 指令1 :sfence 写屏障 在sfence 指令前的写操作 必须在sfence 指令的写操作前完成。
  • 指令2: lfence 读屏障 在lfence指令前的读操作 必须在lfence指令的读操作前完成。
  • 指令3:mfence 读写屏障 在mfence 指令的读写操作 必须在mfence指令的读写操作前完成。

5.volatile使用场景

四、内存屏障的标准

硬件上面的内存屏障

先了解两种命令:

  • Store:将处理器缓存的数据刷新到内存中。
  • Load:将内存存储的数据拷贝到处理器的缓存中。

     Store屏障,是x86的”sfence“指令,在其他指令后插入sfence指令,能让当前线程写入高速缓存中的最新数据更新写入主内存,让其他线程可见。

     Load屏障,是x86上的”ifence“指令,在其他指令前插入ifence指令,可以让高速缓存中的数据失效,强制当前线程从主内存里面加载数据。

例如 Intel 硬件的内存屏障的设计比较简单,有四种,三条指令。

  • 指令1 :sfence 写屏障 在sfence 指令前的写操作 必须在sfence 指令的写操作前完成。
  • 指令2: lfence 读屏障 在lfence指令前的读操作 必须在lfence指令的读操作前完成。
  • 指令3:mfence 读写屏障 在mfence 指令的读写操作 必须在mfence指令的读写操作前完成。具备ifence和sfence的能力。
  • Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
Java的内存屏障

     在java里面有4种,就是 LoadLoad,StoreStore,LoadStore,StoreLoad 实际上也能看出来,这四种都是上面的两种的组合产生的。

屏障类型

指令示例

说明

LoadLoad Barriers

Load1;LoadLoad;Load2

该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作

StoreStore Barriers

Store1;StoreStore;Store2

该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作

LoadStore Barriers

Load1;LoadStore;Store2

确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作

StoreLoad Barriers

Store1;StoreLoad;Load2

该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

     StoreLoad Barriers 同时具备其他三个屏障的效果,因此也称之为全能屏障(mfence),是目前大多数处理器所支持的;但是相对其他屏障,该屏障的开销相对昂贵。

     然而,除了mfence,不同的CPU架构对内存屏障的实现方式与实现程度非常不一样。x86架构是在多线程编程中最常见的,下面讨论x86架构中内存屏障的实现。

五、X86架构的内存屏障

Intel X86架构的内存屏障的设计比较简单,只有三条指令。

  • Store Barrier 指令1 :sfence指令实现了Store Barrier,相当于StoreStore Barriers。sfence写屏障在sfence指令前的写操作必须在sfence 指令的写操作前完成。
  • **Load Barrier **指令2:lfence指令实现了Load Barrier,相当于LoadLoad Barriers。lfence 读屏障 在lfence指令前的读操作 必须在lfence指令的读操作前完成。
  • Full Barrier 指令3:mfence指令实现了Full Barrier,相当于StoreLoad Barriers。mfence 读写屏障 在mfence 指令的读写操作 必须在mfence指令的读写操作前完成。

所以说如下详细介绍:

Store Barrier

     sfence指令实现了Store Barrier,相当于StoreStore Barriers。

     强制所有在sfence指令之前的store指令,都在该sfence指令执行之前被执行,发送缓存失效信号,并把store buffer中的数据刷出到CPU的L1 Cache中;所有在sfence指令之后的store指令,都在该sfence指令执行之后被执行。即,禁止对sfence指令前后store指令的重排序跨越sfence指令,使所有Store Barrier之前发生的内存更新都是可见的

Load Barrier

     lfence指令实现了Load Barrier,相当于LoadLoad Barriers。

     强制所有在lfence指令之后的load指令,都在该lfence指令执行之后被执行,并且一直等到load buffer被该CPU读完才能执行之后的load指令(发现缓存失效后发起的刷入)。即,禁止对lfence指令前后load指令的重排序跨越lfence指令,配合Store Barrier,使所有Store Barrier之前发生的内存更新,对Load Barrier之后的load操作都是可见的

Full Barrier

     mfence指令实现了Full Barrier,相当于StoreLoad Barriers。

     mfence指令综合了sfence指令与lfence指令的作用,强制所有在mfence指令之前的store/load指令,都在该mfence指令执行之前被执行;所有在mfence指令之后的store/load指令,都在该mfence指令执行之后被执行。即,禁止对mfence指令前后store/load指令的重排序跨越mfence指令,使所有Full Barrier之前发生的操作,对所有Full Barrier之后的操作都是可见的。

六、volatile引出的可见性和重排序问题,内存屏障是如何解决的

以x86架构为例,JVM对volatile变量的处理如下:

  • 在写volatile变量v之后,插入一个sfence。这样,sfence之前的所有store(包括写v)不会被重排序到sfence之后,sfence之后的所有store不会被重排序到sfence之前,禁用跨sfence的store重排序;且sfence之前修改的值都会被写回缓存,并标记其他CPU中的缓存失效。
  • 在读volatile变量v之前,插入一个lfence。这样,lfence之后的load(包括读v)不会被重排序到lfence之前,lfence之前的load不会被重排序到lfence之后,禁用跨lfence的load重排序;且lfence之后,会首先刷新无效缓存,从而得到最新的修改值,与sfence配合保证内存可见性。

在另外一些平台上,JVM使用mfence代替sfence与lfence,实现更强的语义。

二者结合,共同实现了Happens-Before关系中的volatile变量规则。

八、CAS

     在x86架构上,CAS被翻译为"lock cmpxchg..."。cmpxchg是CAS的汇编指令。在CPU架构中依靠lock信号保证可见性并禁止重排序。

     lock前缀是一个特殊的信号,执行过程如下:

  • 对总线和缓存上锁。
  • 强制所有lock信号之前的指令,都在此之前被执行,并同步相关缓存。
  • 执行lock后的指令(如cmpxchg)。
  • 释放对总线和缓存上的锁。
  • 强制所有lock信号之后的指令,都在此之后被执行,并同步相关缓存。

     因此,lock信号虽然不是内存屏障,但具有mfence的语义(当然,还有排他性的语义)。与内存屏障相比,lock信号要额外对总线和缓存上锁,成本更高。

九、锁

     JVM的内置锁通过操作系统的管程实现。且不论管程的实现原理,由于管程是一种互斥资源,修改互斥资源至少需要一个CAS操作。因此,锁必然也使用了lock信号,具有mfence的语义。

     锁的mfence语义实现了Happens-Before关系中的监视器锁规则。

CAS具有同样的mfence语义,也必然具有与锁相同的偏序关系。尽管JVM没有对此作出显式的要求。