一、什么是内存屏障

内存屏障(memory barrier)是一个CPU指令。内存屏障,有的也成为内存栅栏或者内存篱笆。

JVM内存屏障两边的指令不可以重排序。

1.1 硬件层级的内存屏障

Intel硬件提供了一系列的内存屏障,主要有: 

内存屏障分为读屏障(lfence--即Load fence)、写屏障(sfence--即Save fence)和全屏障(mfence)。

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.2 JSR内存屏障

不同硬件实现内存屏障的方式不同,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指令读取之前对其他处理器可见。

简而言之,保证内存屏障前后指令顺序执行,防止重排序。

二、Java内存屏障的使用

Java中内存屏障的使用,主要包括Synchronized、volatile和Unsafe的情况。

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

2.1 Volatile基本介绍

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

2.1.1 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中对应的缓存行无效。
一个处理器的缓存回写到内存会导致其他处理器的缓存失效。

public class Sington {
    /**
     * 计数
     */
    private int count;

    private static volatile Sington sInstance;

    private Sington() {
        count = 10;
    }

    public static Sington getInstance() {
        if (sInstance == null) {
            synchronized (Sington.class) {
                if (sInstance == null) {
                    // 初始化(写入操作)
                    sInstance = new Sington();
                }
            }
        }
        return sInstance;
    }

    public int getCount() {
        return count;
    }
}
17 new #3 <com/joe/helloapp/Sington>
20 dup
21 invokespecial #4 <com/joe/helloapp/Sington.<init>>
24 putstatic #2 <com/joe/helloapp/Sington.sInstance>
27 aload_0

包含三个步骤:

  1. new 对象分配内存空间(L17),此时开辟内存以后,对象成员都是未初始化状态,称为半初始化状态。count = 0
  2. 执行对象初始化处理(L21),init函数初始化生成的内存对象初始化以后,count=10
  3. 建立引用关系,sInstance指向创建好的对象.

java 读屏障 写屏障 java内存屏障_java 读屏障 写屏障

图1

java 读屏障 写屏障 java内存屏障_java_02

图2

java 读屏障 写屏障 java内存屏障_java_03

图3

单例对象初始化的时候,这个一个写操作,为了防止对象初始化出现重排序(2和3重排序),导致多线程返回的是一个未初始化的对象,出现数据不一致问题。

2.1.2 Volatile实现禁用重排序

volatile关键字通过“内存屏障”来防止指令被重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。
下面是基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。

java 读屏障 写屏障 java内存屏障_java_04

2.1.3 volatile的hotSpot实现

java 读屏障 写屏障 java内存屏障_内存屏障_05

hotspot中,volatile使用的是lock的方式实现,类似于内存屏障的功能。