java内存屏障的原理与应用

目录

1. java内存屏障

2.java内存屏障的使用

一. java内存屏障

1.1 什么是内存屏障(Memory Barrier)?
内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。


1.2 为什么需要内存屏障
我们知道,在多CPU(核)场景下,为了充分利用CPU,会通过流水线将指令并行进行。为了能并行执行,又需要将指令进行重排序以便进行并行执行,那么问题来了,那些指令不是在所有场景下都能进行重排,除了本身的一些规则(如Happens Before 规则)之外,我们还需要确保多CPU的高速缓存中的数据与内存保持一致性, 不能确保内存与CPU缓存数据一致性的指令也不能重排,内存屏障正是通过阻止屏障两边的指令重排序来避免编译器和硬件的不正确优化而提出的一种解决办法
 

1.3 硬件层的内存屏障
Intel硬件提供了一系列的内存屏障,主要有: 
1. lfence,是一种Load Barrier 读屏障 
2. sfence, 是一种Store Barrier 写屏障 
3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力 
4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
 

1.4 内存屏障的主要类型
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。 

Java内存屏障主要有Load和Store两类。 
对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据 
对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存

对于Load和Store,在实际使用中,又分为以下四种:

LoadLoad 屏障 
序列:Load1,Loadload,Load2 
确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。

StoreStore 屏障 
序列:Store1,StoreStore,Store2 
确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。

LoadStore 屏障 
序列: Load1; LoadStore; Store2 
确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。

StoreLoad 屏障 
序列: Store1; StoreLoad; Load2 
确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。

二. java内存屏障的使用

2.1 java内存屏障使用介绍

常见的有以下几种:

a. 通过 Synchronized关键字包住的代码区域,当线程进入到该区域读取变量信息时,保证读到的是最新的值.这是因为在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到内存中,而对数据的读取也不能从缓存读取,只能从内存中读取,保证了数据的读有效性.这就是插入了StoreStore屏障
b. 使用了volatile修饰变量,则对变量的写操作,会插入StoreLoad屏障.
c. 其余的操作,则需要通过Unsafe这个类来执行.
    UNSAFE.putOrderedObject类似这样的方法,会插入StoreStore内存屏障 
    Unsafe.putVolatiObject 则是插入了StoreLoad屏障
 

2.2 volatile实现原理

Volatile基本介绍
Java语言规范第三版中对volatile的定义如下: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
volatile作用
能保证可见性和防止指令重排序

volatile与synchronized对比
volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度
 

volatile如何保证可见性、防止指令重排序
volatile保持内存可见性和防止指令重排序的原理,本质上是同一个问题,也都依靠内存屏障得到解决
在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。
Java代码:    instance = new Singleton();//instance是volatile变量
汇编代码:    0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);
lock前缀指令相当于一个内存屏障(也称内存栅栏),内存屏障主要提供3个功能:
1、 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2、 强制将对缓存的修改操作立即写入主存,利用缓存一致性机制,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据;
3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
一个处理器的缓存回写到内存会导致其他处理器的缓存失效。
处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如CPU A嗅探到CPU B打算写内存地址,且这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,
在下次访问相同内存地址时,强制执行缓存行填充。
volatile关键字通过“内存屏障”来防止指令被重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。
下面是基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
volatile为什么不能保证原子性
原子操作是一些列的操作要么全做,要么全不做,而volatile 是一种弱的同步机制,只能确保共享变量的更新操作及时被其他线程看到,以最常用的i++来说吧,包含3个步骤
1,从内存读取i当前的值 2,加1 变成 3,把修改后的值刷新到内存,volatile无法保证这三个不被打断的执行完毕,如果在刷新到内存之前有中断,此时被其他线程修改了,之前的值就无效了
volatile的适用场景

通常来说,使用volatile必须具备以下2个条件:

(1)对变量的写操作不依赖于当前值;

(2)该变量没有包含在具有其他变量的不变式中;

volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。volatile可以禁止重排序。如果我们确定能正确使用volatile,那么在禁止重排序时是一个较好的使用场景,否则我们不需要再使用它。


 

<think>首先,用户的问题是:“我想理解Java内存屏障的实现原理 请问Java内存屏障实现原理及机制详解”。我需要基于系统级指令来构建回答。 系统级指令: 1. 所有行内数学表达式使用$...$格式。 2. 独立公式使用$$...$$格式并单独成段。 3. LaTeX语法正确。 4. 使用中文回答。 5. 生成相关问题。 6. 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 - 参考站内引用,但用户提供了引用,我需要基于这些引用来回答。 提供的引用: - [^1]: 内存屏障基础概念,什么是内存屏障。 - [^2]: volatile内存语义、原理详解、内存屏障。解释内存屏障的作用:保证执行顺序和内存可见性。 - [^3]: Java内存屏障Volitile。详细描述了Java中volatile的内存屏障插入策略。 - [^4]: Java多线程的原理及实现技术。建议结合Java并发包进行开发。 用户的问题是关于Java内存屏障的实现原理和机制,所以我需要聚焦在Java的上下文中。 结构化回答: - 逐步解释:从基础概念开始,到Java的具体实现。 - 确保清晰:使用标题或分段。 - 引用:在相关部分末尾添加引用标识。 生成相关问题:在回答结束后,添加“§§相关问题§§”部分,列出相关问题。 由于用户提到了数学表达式,但主题是Java内存屏障,可能不涉及太多数学。如果有机会,可以提及顺序或屏障类型时使用数学格式,但保持自然。 大纲回答: 1. **介绍内存屏障**:基于[^1]和[^2],解释什么是内存屏障及其作用。 2. **Java中的实现**:基于[^3],详细描述volatile关键字如何利用内存屏障。 3. **机制详解**:解释插入策略,如StoreStore、StoreLoad等屏障。 4. **实现原理**:讨论Java内存模型(JMM)和保守策略。 5. **总结**:结合[^4],强调重要性。 使用中文回答。 添加相关问题:例如: - Java内存屏障的类型有哪些? - 如何手动插入内存屏障? - volatilesynchronized的区别? - 内存屏障在并发编程中的应用场景? 确保引用标识自然添加。例如,在解释概念时,如果基于引用,在句子末尾加[^1]。 现在,构建回答。</think>### Java内存屏障实现原理及机制详解 在Java并发编程中,内存屏障(Memory Barrier)是一种关键同步机制,用于控制内存访问的顺序和可见性,确保多线程环境下的数据一致性。Java通过Java内存模型(JMM)定义了内存屏障的行为,主要依赖于`volatile`关键字和底层指令(如x86的`lock`前缀)来实现。下面我将逐步解释内存屏障的实现原理和机制,帮助您深入理解。 #### 1. **内存屏障基础概念** 内存屏障是一种CPU指令,用于强制内存操作的顺序和可见性。它有两个核心作用: - **保证执行顺序**:防止编译器和处理器对指令进行重排序优化,确保屏障前后的指令按程序逻辑执行。 - **保证内存可见性**:强制刷新CPU缓存,使所有线程能立即读取共享变量的最新值。 例如,在Java中,内存屏障确保对`volatile`变量的写操作对所有线程立即可见[^1][^2]。 #### 2. **Java内存屏障的实现原理** Java通过`volatile`关键字隐式插入内存屏障,其实现基于JMM(Java Memory Model)的保守策略。编译器在生成字节码时,会在特定位置插入屏障指令,这些指令最终映射到CPU的硬件屏障(如x86的`mfence`)。关键点包括: - **底层机制**:当Java代码使用`volatile`变量时,JVM在编译期插入内存屏障指令;运行时,这些指令被翻译为CPU指令(如`lock`前缀),强制同步主内存和CPU缓存。例如,`lock`指令会锁住总线,阻止其他处理器访问内存,确保数据一致性[^2][^3]。 - **JMM的保守策略**:由于优化屏障插入的复杂性,JMM采用保守方法,总是插入所有必要屏障,以避免重排序和可见性问题。这牺牲了少许性能,但保证了可靠性[^3]。 #### 3. **内存屏障的机制详解** Java为`volatile`变量定义了四种屏障类型,分别在读/写操作前后插入。这些屏障控制指令顺序和内存更新: - **StoreStore屏障**:插入在`volatile`写操作之前。确保所有普通写操作(非`volatile`)在`volatile`写之前完成。例如: ```java // 伪代码:StoreStore屏障 normalWrite(); // 普通写操作 StoreStoreBarrier(); // 屏障:确保normalWrite()在volatile写前完成 volatileVar = value; // volatile写操作 ``` - **StoreLoad屏障**:插入在`volatile`写操作之后。确保`volatile`写操作完成后,任何后续读操作都能看到最新值。这是最重的屏障,因为它强制刷新整个缓存[^3]。 - **LoadLoad屏障**:插入在`volatile`读操作之后。确保所有后续读操作在`volatile`读之后执行,防止重排序。 - **LoadStore屏障**:插入在`volatile`读操作之后。确保读操作完成后,任何后续写操作不会重排序到读之前。 这些屏障的组合确保了`volatile`变量的语义: - **写操作序列**:`StoreStore` + `volatile写` + `StoreLoad`。例如,在写`volatile`变量时,屏障防止普通写被重排序到`volatile`写之后。 - **读操作序列**:`volatile读` + `LoadLoad` + `LoadStore`。例如,在读`volatile`变量时,屏障确保后续操作不会乱序[^3]。 数学上,内存屏障确保操作顺序满足程序顺序:设$P$为程序指令序列,屏障强制$P_i$和$P_j$(屏障前后的指令)满足$i < j$时,$P_i$在$P_j$前执行。这避免了重排序导致的不一致。 #### 4. **实际应用性能影响** 在Java中,内存屏障主要用于`volatile`变量和`java.util.concurrent`包(如`AtomicInteger`)。例如: - **volatile示例**:声明`volatile int counter;`时,JVM自动插入屏障,保证线程安全。 - **性能考量**:内存屏障会引入开销(如缓存刷新),但现代JVM优化了插入策略。在高并发场景,建议结合`java.util.concurrent`工具(如`ReentrantLock`),而非过度依赖`volatile`[^4]。 #### 总结 Java内存屏障通过JMM和`volatile`关键字实现,核心是插入四种屏障指令来禁止重排序和保证可见性。其机制依赖于编译器和CPU的协作,采用保守策略确保可靠性。理解这一点,有助于设计高效并发程序。结合Java并发包(如`java.util.concurrent`)可以进一步优化性能[^3][^4]。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值