Reactor响应式编程系列(九)- Reactor的顶梁柱QueueSubscription以及无界队列
QueueSubscription
主要用来干什么?它主要用于对那些内部有队列支持的Subscription
进行优化。
在前面的几篇文章中,提到过多次QueueSubscription
以及无界队列,在讲publishOn
和subscribeOn
的时候,也提到过所谓的异步指的是元素的存储和获取过程异步,那么这个存储的地方也就是所谓的队列。因此接下来来看下无界队列: SpscLinkedArrayQueue
一. 无界队列SpscLinkedArrayQueue
来看下这个类的相关结构:
final class SpscLinkedArrayQueue<T> extends AbstractQueue<T>
implements BiPredicate<T, T> {
final int mask;
volatile long producerIndex;
@SuppressWarnings("rawtypes")
static final AtomicLongFieldUpdater<SpscLinkedArrayQueue> PRODUCER_INDEX =
AtomicLongFieldUpdater.newUpdater(SpscLinkedArrayQueue.class,
"producerIndex");
// 1.原子类型的数组,其最后一个位置会单独空出来,用于存储下一个数组的引用。
// 若即将存满元素的时候,最后一个元素会存出下个数组的引用和标志位Next,这个Next就和我们元素存储的位置相关。
AtomicReferenceArray<Object> producerArray;
//..
static final Object NEXT = new Object();
SpscLinkedArrayQueue(int linkSize) {
// 2.从给定的值中找出下一个较大的正值,并且该值应该是2的N次方
int c = Queues.ceilingNextPowerOfTwo(Math.max(8, linkSize));
// 3.由于第一点,因此最终的数组长度是c+1,但是其实际存储的元素数量仅仅为c-1.
// 有一个用于存储数组引用,一个用于存储标志位,也就是需要腾出两个位置,故下面的mask=c - 1
this.producerArray = this.consumerArray = new AtomicReferenceArray<>(c + 1);
// 4.同时设定一个变量mask,finla类型,通过二进制计算来专门确定当前下发元素在数组中要存储的位置
this.mask = c - 1;
}
该类针对多线程下的线性安全,会分别对索引producerIndex
以及producerArray
进行原子化,并设计一个无界队列,我们来看下其offer()
方法:
@Override
public boolean offer(T e) {
Objects.requireNonNull(e);
// 此处的 index 是要存储元素在整个无界队列中的位置
long pi = producerIndex;
AtomicReferenceArray<Object> a = producerArray;
int m = mask;
// 1.计算插入元素在该数组中的下一个位置的偏移量
int offset = (int) (pi + 1) & m;
// 2.若当前执行的操作是添加操作,但是下一个位置中却有了元素,说明了什么?
// 说明了数组已经满了,那么就需要创建新的数组b
if (a.get(offset) != null) {
// 3.重新计算插入元素在新数组的偏移量位置
offset = (int) pi & m;
AtomicReferenceArray<Object> b = new AtomicReferenceArray<>(m + 2);
producerArray = b;
b.lazySet(offset, e);
a.lazySet(m + 1, b);// 原数组的最后一个位置存储新数组的引用
a.lazySet(offset, NEXT);// 在原数组内的这个位置存储 NEXT 标志位常量元素
PRODUCER_INDEX.lazySet(this, pi + 1);
}
else {
offset = (int) pi & m;
a.lazySet(offset, e);
PRODUCER_INDEX.lazySet(this, pi + 1);
}
return true;
}
而之所以要求使用 2^n
的长度,是因为在长度减1之后才能得到一个以0开头后面全都是1的二进制数,在使用任意合法范围内的正整数与之进行&运算时, 所得结果都会在0至 2^n
范围内,并且计算速度更快。
总结1:
总结就是:
SpscLinkedArrayQueue
无界队列的实现是通过将多个原子类数组串在一起而成。- 每个数组的长度假设为
n+1
,那么实际上存储的元素个数为n-1
,其中有两个位置用来存储:1.下一个数组地址的引用。2.当前插入的元素需要在下个数组中存储的位置Next。 - 同时用全局变量
mask
去标识当前下发元素在数组中要存储的位置。 - 由于下标、数组都是原子类的, 故底层操作能够保证线性安全,通过Next地址的改变以及数组中的最后位用于存储下个数组的地址,来让这个队列无限的延伸下去,达到一个穿针引线的作用。
二. QueueSubscription.requestFusion的催化效应
有时候队列只是为了适应一个架构,而并不会真正的去产生、存储和获取元素。那么在基于状态值感应的情况下去下发元素的时候,确确实实不需要去存储元素。但是多线程请求下很容易发生状态值的异常,那么如何将消费控制在一个单线程中,保证接受多个线程同时请求下发元素?
因此先来看下一段代码:
public interface Fuseable {
interface QueueSubscription<T> extends Queue<T>, Subscription {
int requestFusion(int requestedMode);// 用于策略判断
@Override
@Nullable
default T peek() {
throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE);
}
@Override
default boolean add(@Nullable T t) {
throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE);
}
@Override
default boolean offer(@Nullable T t) {
throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE);
}
}
}
看到QueueSubscription
类中的3个方法,很奇怪对不对?默认都是抛出异常,它们就是用来个给QueueSubscription
接口定基调的方法,即告诉我们这个接口并不支持背压操作。它需要一个用来判断是同步还是异步请求拉取策略的方法,也就是requestFusion()
官网注释中对这个方法的翻译就是:
- 作为一个订阅者,下游操作会调用上一个操作实现的
requestFusion()
方法。 - 你传入的参数所代表的模式可以从
SYNC
,ASYNC
orANY
任选一种(不可为NONE
)。 - 而上游的操作对于
requestFusion()
方法的具体实现,返回的模式结果应该从NONE
,SYNC
orASYNC
中选择一个返回(不可为ANY
)。
案例如下:查看FluxGenerate.requestFusion()
的实现:
@Override
public int requestFusion(int requestedMode) {
if ((requestedMode & Fuseable.SYNC) != 0 && (requestedMode & Fuseable.THREAD_BARRIER) == 0) {
outputFused = true;
return Fuseable.SYNC;
}
return Fuseable.NONE;
}
可以发现,策略是返回一个Fuseable.NONE
,即对应的GeneratorSubscription
在非特殊情况下对模式不会做出任何的设定,而对于其他类型的Subscription
就会根据对应的策略,来对异步、同步的策略作出对应的回应,去控制对应的消费动作。避免产生多线程下的状态异常。
总结2:
- 面对可能是异步、同步的源,即不同类型源的创建,其会对应着不同类型的
xxxSubscription
,其都会实现QueueSubscription
接口。 - 由于状态值在多线程情况下的改变可能会导致异常,因此各个子类通过实现对应的方法
requestFusion()
来判断各自的请求拉取策略,判断是同步的还是异步的。从而判断是否支持背压操作。 - 可以通过这种催化方式,保证上下链中,将消费控制在一个单线程中,保证接受多个线程同时请求下发元素。