内存屏障(Memory Barrier)概述
内存屏障(Memory Barrier),也叫做 内存栅栏(Memory Fence),是现代计算机体系结构中的一种机制,用于控制指令执行的顺序,以确保在多核处理器环境中,多个线程或处理器对共享内存的操作能够按照特定的顺序进行。
在并发编程中,尤其是在多线程程序中,我们需要确保不同线程间的内存可见性和操作顺序。CPU 和编译器为了优化性能,常常会对指令执行进行重新排序,这会导致在多线程环境下某些操作的执行顺序不符合预期,从而产生竞态条件或内存可见性问题。
为什么需要内存屏障?
内存屏障有两个作用
1、阻止屏障两侧的指令重排序
2、强制把缓冲区/高速缓存中的脏数据写回主内存,让缓存中相应的数据失效。
在多核系统中,每个核心都有自己的缓存(L1、L2、L3 等)。当一个线程在某个核心上对共享变量进行修改时,其他核心可能并没有及时感知到这个修改。另外为了提高性能,现代处理器和编译器会重排指令的执行顺序,例如将某些内存操作提前或延后执行,导致在多线程环境下可能出现读取到过时数据的问题。
内存屏障通过强制CPU按照特定的顺序执行内存操作,避免了指令重排带来的问题,确保线程间的内存可见性和操作顺序。
内存屏障的类型
内存屏障一般有以下几种类型:
内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障的实现方式
在不同的硬件平台上,内存屏障的实现方式不同。通常,内存屏障依赖于特定的硬件指令或者特定的 CPU 特性。
在 Java 中,内存屏障的实现
java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile
的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
示例
假设我们有两个线程,线程 A 和线程 B。线程 A 先写入数据,然后通知线程 B 读取数据。为了确保线程 B 能够及时看到线程 A 的写入数据,我们需要确保操作的顺序性。
1 | class SharedResource { |
在上面的代码中:
flag
被声明为volatile
,这会确保当线程 A 更新flag
的值时,线程 B 能立即看到这个值的变化,避免了线程 B 因为缓存问题一直读取不到最新的flag
值。volatile
在底层插入了相应的内存屏障,确保了对flag
的操作顺序和内存可见性。
总结
内存屏障是并发编程中确保多线程之间内存可见性和操作顺序性的重要机制。它通过强制限制 CPU 和编译器的指令重排,确保线程在访问共享数据时,操作的顺序是符合预期的。
- 内存屏障的种类:包括
Store Barrier
(写屏障)、Load Barrier
(读屏障) - Java 中的内存屏障:通过
volatile
关键字、Atomic
类等方式来确保多线程环境中的可见性和顺序性,底层依赖于内存屏障。
通过理解内存屏障的原理,我们可以更好地设计高效、线程安全的并发程序。