众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
其中:编译程序优化指令执行次序指的是,JVM在真正执行代码的时候并不一定按照我们写好的代码顺序去运行指令, 这里可能会发生指令重排序(Instruction Reorder)。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
内存屏障(Memory Barrier),也称为内存栅栏或内存栅障,是一种特殊的硬件指令,用于在多核处理器系统中同步对内存的访问。内存屏障的主要作用是确保内存操作的顺序性,防止指令重排序和缓存一致性问题,从而在多线程环境中保持数据的正确性和可见性。
在现代计算机系统中,CPU为了提高性能,会采用指令重排序和缓存机制。指令重排序是编译器和处理器为了优化执行效率而进行的一种行为,它们可能会改变指令的执行顺序。缓存机制则是为了减少
CPU访问主内存的次数,提高数据访问速度而采用的局部存储技术。
然而,这些优化在单线程环境中不会引起问题,因为重排序后的执行结果与原始代码的语义相同,且缓存机制确保了数据的局部性。但在多线程环境中,这些问题就会变得复杂。一个线程对数据的修改可能由于缓存未及时刷新到主内存,导致其他线程读取到旧的数据。此外,指令重排序可能会导致一个线程看到一个未完全构造的对象,因为对象的初始化代码被重排序到了对象引用赋值之后。
内存屏障就是为了解决这些问题而设计的。它提供了一种机制,可以强制执行某些内存操作,确保这些操作的顺序性和可见性。内存屏障分为两种类型:
内存屏障的使用通常需要编程者或编译器具备相关知识,以便在关键代码段插入屏障指令,确保多线程之间的数据同步。在硬件层面,内存屏障通常由处理器提供,并且在执行时可能会伴随着缓存刷新和/或指令重排序的抑制。
在Java中,内存屏障的实现是通过Java虚拟机(JVM)来完成的。JVM在执行Java程序时,会根据程序的语义和硬件平台的特性来插入内存屏障指令。这些指令确保了内存操作的顺序性和数据的可见性,特别是在多线程环境中。
Java中的内存屏障主要与volatile关键字和同步代码块(synchronized blocks)有关。