内存屏障是个啥?

​ 内存屏障,是计算机CPU在多核时代,通过数据拷贝,将一个处理器的内存状态对其他处理器可见。在百度百科中,内存屏障也被称呼为内存栅栏,内存栅障,屏障指令等。乍一看到内存栅栏这个这个名词,我还以为这个是跟Java栅栏是一个东西,但是实际上两者并不相同。
在执行CPU指令的时候,CPU会对指令进行乱序执行,来优化性能(这一点我在《共享内存模型》一文中有提到过)。而在多线程的时候,乱序执行可能会导致代码的执行跟预期结果不同,内存屏障同时阻止指令排序行为的发生。

内存屏障有啥用?

​ 在CPU层次,内存屏障分为两种:读屏障 Load Barrier 和写屏障 Store Barrier。读屏障表示在线程读取前,将主存的内容载入到缓存或者是线程内存里。写屏障表示在缓存或者线程内存里插入后,将更新写入到主内存里。
通过内存屏障,主内存和缓存(线程内存)之间做到了数据统一,保证了可见性。也就是说通过内存屏障,已经达成了共享内存模型的可见性要求。

内存屏障的缺陷

​ 通过内存屏障,保证了数据可见性,同时防止屏障两端指令的重排序。但是内存屏障做不到并发过程中的原子性,这个也就是volatile关键字仅仅能够提供可见性,却不能够提供原子性的原因,因为volatile的实现就是通过内存屏障来进行实现的。

内存屏障做不到原子性的原因:

当存在两个线程Ta和Tb以及一个共享变量count=0,第一时刻Ta读取了count的值,这个读操作采用了内存屏障,读取前将Ta线程的内存里的数据清空,然后将主存的count复制到Ta线程的内存。
Ta线程执行count的++操作,此时Tb线程读取count的值,此时主存内count的值为0。
Ta执行变量count的++操作结束,将数据写回。同时Tb将读取的count执行++操作。此时主存内count的值为1。
Tb将数据写回。此时主存内的count值仍为1。

​ 如果程序的执行是正确的,那么最终结果应该是count=2,但是因为在程序执行过程中不提供原子性支持,所以会出现错误。
这一部分内容都是CPU层次的内存屏障,下面的内容是Java层次的内存屏障。

Java内存屏障

​ Java内存屏障包括四种:LoadStoreStoreStoreLoadLoadStoreLoad。也就是CPU层次的内存屏障两两结合。

LoadLoad : 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore : 对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore : 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad : 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能,但是开销相对较大。

JVM层次对于内存屏障的实现

volatile语义中的内存屏障

​ volatile对于内存屏障实现是悲观的,对于volatile变量:

每次写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障
每次读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障

​ 同样的,因为内存屏障的缺陷,volatile不能够表现出原子性的特性,仅仅能够提供内存可见性。

final语义中的内存屏障

​ 在final域里,分别针对屏障做出了规则限制:

写final域

JMM禁止编译器把final域的写重排序到构造函数外。
编译器会在final域的写之后,return之前,插入StoreStore屏障。

读final域

初次读对象引用域初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。

​ 也就是说,对于final域,总是在一个对象的所有final域写入完毕后才能读取和引用。