内存屏障的整理

为何会有内存屏障?java

为了提高数据加载速度有了CPU缓存,在多核状况下带来了缓存一致性问题,能够经过MESI缓存一致性协议来解决。但MESI缓存一致性协议下,一个CPU可能须要等待另外一个CPU响应后才能继续执行,致使了阻塞,影响性能(能够参考以前voltile那篇文章,由于涉及到总线加锁或者缓存锁定)。因此增长了StoreBuffer和InvalidateQueue,也就是须要store时先放到StoreBuffer里,而后继续执行下一条指令,等到其余CPU响应返回后再处理对应store;收到invalidate通知时也不当即处理,而是先放到InvalidateQueue,并当即给予对方响应,而后等到合适时机再一块儿处理。这种优化提高了CPU执行能力,但也使得MESI协议的操做没法当即获得处理,产生了可见性和有序性问题。编程

什么是内存屏障?缓存

它是一个CPU指令。它是这样一条指令:安全

a)确保一些特定操做执行的顺序。并发

b)影响一些数据的可见性(多是某些指令执行后的结果)。性能

有序性:编译器和CPU能够在保证输出结果同样的状况下对指令重排序,使性能获得优化。插入一个内存屏障,至关于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。优化

可见性:内存屏障另外一个做用是强制更新一次不一样CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将获得最新值,而不用考虑究竟是被哪一个cpu核心或者哪颗CPU执行的。线程

内存屏障的种类设计

LoadLoad 屏障3d

序列: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指令读取以前对其余处理器可见。StoreLoad屏障能够防止一个后续的load指令 不正确的使用了Store1的数据,而不是另外一个处理器在相同内存位置写入一个新数据。正由于如此,因此在下面所讨论的处理器为了在屏障前读取一样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎全部的现代多处理器中都须要使用,但一般它的开销也是最昂贵的。它们昂贵的部分缘由是它们必须关闭一般的略过缓存直接从写缓冲区读取数据的机制。这可能经过让一个缓冲区进行充分刷新(flush),以及其余延迟的方式来实现。

volatile语义中的内存屏障

volatile的内存屏障策略很是严格保守,很是悲观且毫无安全感的心态:

在每一个volatile写操做前插入StoreStore屏障,在写操做后插入StoreLoad屏障;

在每一个volatile读操做前插入LoadLoad屏障,在读操做后插入LoadStore屏障;

因为内存屏障的做用,避免了volatile变量和其它指令重排序、线程之间实现了通讯,使得volatile表现出了锁的特性。

final语义中的内存屏障

对于final域,编译器和CPU会遵循两个排序规则:

新建对象过程当中,构造体中对final域的初始化写入和这个对象赋值给其余引用变量,这两个操做不能重排序;(废话嘛)

初次读包含final域的对象引用和读取这个final域,这两个操做不能重排序;(晦涩,意思就是先赋值引用,再调用final值)

总之上面规则的意思能够这样理解,必需保证一个对象的全部final域被写入完毕后才能引用和读取。这也是内存屏障的起的做用:

写final域:在编译器写final域完毕,构造体结束以前,会插入一个StoreStore屏障,保证前面的对final写入对其余线程/CPU可见,并阻止重排序。

读final域:在上述规则2中,两步操做不能重排序的机理就是在读final域前插入了LoadLoad屏障。

X86处理器中,因为CPU不会对写-写操做进行重排序,因此StoreStore屏障会被省略;而X86也不会对逻辑上有前后依赖关系的操做进行重排序,因此LoadLoad也会变省略。

对性能的影响

内存屏障做为另外一个CPU级的指令,没有锁那样大的开销。内核并无在多个线程间干涉和调度。但凡事都是有代价的。内存屏障的确是有开销的——编译器/cpu不能重排序指令,致使不能够尽量地高效利用CPU,另外刷新缓存亦会有开销。因此不要觉得用volatile代替锁操做就一点事都没。

你会注意到Disruptor的实现对序列号的读写频率尽可能降到最低。对volatile字段的每次读或写都是相对高成本的操做。可是,也应该认识到在批量的状况下能够得到很好的表现。若是你知道不该对序列号频繁读写,那么很合理的想到,先得到一整批Entries,并在更新序列号前处理它们。这个技巧对生产者和消费者都适用。如下的例子来自BatchConsumer:

long nextSequence = sequence + 1;
    while (running)
    {
        try
        {
            final long availableSequence = consumerBarrier.waitFor(nextSequence);
            while (nextSequence <= availableSequence)
            {
                entry = consumerBarrier.getEntry(nextSequence);
                handler.onAvailable(entry);
                nextSequence++;
            }
            handler.onEndOfBatch();
            sequence = entry.getSequence();
        }
        catch (final Exception ex)
        {
            exceptionHandler.handle(ex, entry);
            sequence = entry.getSequence();
            nextSequence = entry.getSequence() + 1;
        }
    }

在上面的代码中,咱们在消费者处理entries的循环中用一个局部变量(nextSequence)来递增。这代表咱们想尽量地减小对volatile类型的序列号的进行读写。

总结

内存屏障是CPU指令,它容许你对数据何时对其余进程可见做出假设。在Java里,你使用volatile关键字来实现内存屏障。使用volatile意味着你不用被迫选择加锁,而且还能让你得到性能的提高。

可是,你须要对你的设计进行一些更细致的思考,特别是你对volatile字段的使用有多频繁,以及对它们的读写有多频繁。

参考资料

【并发编程概述:内存屏障、volatile、原子变量和互斥锁】https://qilu.me/2019/03/16/2019-03-16/

【内存屏障】https://www.jianshu.com/p/2ab5e3d7e510

【剖析Disruptor:为何会这么快?(三)揭秘内存屏障】http://ifeve.com/disruptor-memory-barrier/

【volatile与内存屏障总结】https://zhuanlan.zhihu.com/p/43526907

相关文章
相关标签/搜索