1. 什么是内存屏障
它是一条CPU指令: a)确保一些特定操作执行的顺序; b)影响一些数据的可见性(可能是某些指令执行后的结果)。
2. 内存屏障与处理器重排序
现代的处理器使用写缓冲区来临时保存向内存写入的数据,每个处理器都有自己的缓冲区。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据产生的阻塞。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区对同一内存地址的多次写,可以减少对内存总线的占用。写缓冲区有很多好处但它只对自己所在的处理器所见。这个特性导致处理器对内存的读/写操作的执行顺序不一定与内存实际发生的读/写顺序一致。如下图:
假设处理器A和处理器B按程序并行执行内存访问,最终可能得到x=y=0的结果。具体原因如下图:
当A2,B2分别先于A3,B3执行(Store优先于Load)时就会产生x=y=0的结果。
由于现代的处理器都会使用写缓冲区,编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。
为了保证内存的可见性,java编译器在生成指令序列的适当位置插入一个内存屏障来禁止特定类型的处理器重排序,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。
java内存模型的内存屏障分为以下4类:
- StoreStore Barriers:确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
- LoadStore Barriers:确保Load1数据装载之前于Store2及所有后续存储指令的存储。
- StoreLoad Barriers:确保Store1数据对其他处理器可见(刷新到内存)之前于Load2及所有后续装载指令的装载。
- LoadLoad Barriers:确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
特殊的是StoreLoad Barriers会使该屏障之前的所有内存访问指令(装载和存储指令)完成之后才执行该屏障之后的内存访问指令,是一个”全能型”的屏障,它同时具有其他三个屏障的效果。
内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到各个CPU的缓存(lazy型刷新, CPU监听数据总线将缓存数据标记为invalid,用到时再重内存中载入),这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
3. 内存屏障与volatile
java中的volatile关键字正是使用了内存屏障。如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。
这意味着如果你对一个volatile字段进行写操作,你必须知道:
1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。
2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
3. 对性能的影响
内存屏障作为另一个CPU级的指令,没有锁那样大的开销。内核并没有在多个线程间干涉和调度。但凡事都是有代价的。内存屏障的确是有开销的——编译器/cpu不能重排序指令,导致不可以尽可能地高效利用CPU,另外刷新缓存亦会有开销。所以不要以为用volatile代替锁操作就一点事都没。