Disruptor高性能之道—内存屏障(Volatile)

399 阅读4分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

目录

一、前言

二、Volatile详解

1、volatile的可见性

2、volatile的原理

三、Volatile在Disruptor中的引用

1、sequence的可见性

2、sequence的妙用

四、ReentrantLock如何确保线程可见性

五、惯例


一、前言

本系列文章前面的文中我们分别从False Sharing和无锁实现两个方面来分析了Disruptor为什么会有如此高的性能。今天我们将从Volatile关键字的应用的角度来分析Disruptor如何实现高性能。

说到Java中的volatile关键字,想必大家都应该很清楚。说到其使用我想很多同学都会说这有什么难的,不就是在域变量前面添加了volatile关键字可以保证变量的可见性么。或者大家会进一步说volatile只能够保证可见性,不能够保证原子性什么的。当然我不得不说大家说的都对,但是我还是想大胆的说“你可能真的不会使用volatile”。不信?那请大家看看如下几个问题:

  1. volatile的可见性怎么理解?
  2. volatile的底层是怎么实现的?
  3. volatile和happen-before的关系?

哈哈哈,是不是蒙圈了?!如果你蒙圈了,那么请静下心来看本文的后续内容。如果你完全没有蒙圈,那么恭喜你,你可以说你已经掌握了volatile关键字的原理和用法了。

二、Volatile详解

1、volatile的可见性

我们首先来看一段比较经典的代码(如下)。如果Writer线程先执行writer方法,然后Read线程执行reader方法。请问下x等于多少?0还是42或者是不确定?如果你能够简单的说出答案,且能够解释具体原因,那么恭喜你,你对volatile应该算是了如指掌了。如果你不能完全确定,那么请继续看后续内容。

大家都应该知道volatile能够确保变量的可见性。那么volatile的可见性具体怎么体现呢?很多同学都会说,被volatile修饰的变量在一个线程A中被修改了,紧接着线程B去读取该变量,能够读取到线程A刚刚修改后的值,而并不会因为CPU各种缓存导致线程B读取不到最新的值,这就是volatile的可见性。这样的描述没有错,但是也不全对。因为他仅仅描述到了volatile可见性的一半。

那么我们现在回到上面代码列出的问题。我现在可以告诉大家,x肯定等于42。为什么呢?因为准确的来说volatile的可见性应该叫做线程可见性。以代码中的例子为例。其实是指当writer线程修改了v(volatile变量)之后,此时writer线程可见的所有变量(比如这里的x=42),在reader线程读取到v之后都是可见的。即reader线程读取到v之后,就肯定能够读取到x=42。如下是官方比较专业的解释。

2、volatile的原理

那么为什么voaltile变量具有线程可见性呢?因为JMM中volatile的写-读是符合Happen-before原则。而所有符合Happen-before原则的场景都能够确保线程可见性。其中除了volatile外,解释与加锁就是大家比较熟知的符合happen-before原则的。

而在volatile的具体实现中,JMM又是通过其抽象内存屏障来确保了其线程可见性。在JMM中内存屏障有如下4种,volatile主要使用了StoreStore和StoreLoad。

JMM中的内存屏障只是对各个处理器的内存屏障的一个抽象,这样是为了让我们不用关系处理器之间的差异与实现。大家如果感兴趣可以去了解下。在x86下是通过Lock前缀指令(x86下的屏障) + 总线锁 + 缓存行锁定来确保volatile的可见性。

三、Volatile在Disruptor中的引用

1、sequence的可见性

在Disruptor中,其实并发访问最大且有访问冲突的就是生产者和消费者的sequence。这个sequence在之前介绍False Sharing的时候也提到过。因为提高它的并发访问效率,所以Disruptor特意为他做了字节填充。同时为了确保这个sequence在生产者和消费者期间能够实时可见。因此Disruptor对其中的value字段设置了volatile标识。

2、sequence的妙用

我们平时在使用volatile修饰的字段的时候,大部分都是简单的赋值与读取操作。大家都觉得这样使用比加锁读写还是快多了。其实volatile还有更快的方式。那就是使用UNSAFE.putOrderedLong来对一个volatile变量赋值。其可以确保有序性,但是不能确保立即的可见性。但是它却可以提供比直接对volatile赋值更好的性能。因为它不需要却可见性,也就不涉及到缓存行立即刷新到主存,也不涉及到其他读取了该变量的缓存行的L1或者L2j级缓存失效。所以,在我们使用volatile的时候,如果有些场景只是需要确保有序性,而不需要可见性,则可以考虑使用unsafe.putOrderedXXX来修改volatile变量。

四、 ReentrantLock如何确保线程可见性

我们在上面有提到释放锁和加锁符合happen-before原则,即其能够确保线程可见性。那么你知道ReentrantLock是如何确保其线程可见性的么?这个问题就留给大家自己探索吧。

五、惯例

如果你对本文有任何疑问或者高见,欢迎添加公众号共同交流探讨(添加公众号可以获得”Java高级架构“上10G的视频和图文资料哦)。