Reactor响应式编程系列(九)- Reactor的顶梁柱QueueSubscription以及无界队列

Reactor响应式编程系列(九)- Reactor的顶梁柱QueueSubscription以及无界队列

Reactor响应式编程系列导航

QueueSubscription主要用来干什么?它主要用于对那些内部有队列支持Subscription进行优化。

在前面的几篇文章中,提到过多次QueueSubscription以及无界队列,在讲publishOnsubscribeOn的时候,也提到过所谓的异步指的是元素的存储和获取过程异步,那么这个存储的地方也就是所谓的队列。因此接下来来看下无界队列: 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:

总结就是:

  1. SpscLinkedArrayQueue无界队列的实现是通过将多个原子类数组串在一起而成。
  2. 每个数组的长度假设为n+1,那么实际上存储的元素个数为n-1,其中有两个位置用来存储:1.下一个数组地址的引用。2.当前插入的元素需要在下个数组中存储的位置Next。
  3. 同时用全局变量mask去标识当前下发元素在数组中要存储的位置
  4. 由于下标、数组都是原子类的, 故底层操作能够保证线性安全,通过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()

官网注释中对这个方法的翻译就是:

  1. 作为一个订阅者,下游操作会调用上一个操作实现的requestFusion()方法。
  2. 你传入的参数所代表的模式可以从 SYNC, ASYNC or ANY 任选一种(不可为NONE)。
  3. 而上游的操作对于requestFusion()方法的具体实现,返回的模式结果应该从NONE, SYNC or ASYNC中选择一个返回(不可为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:

  1. 面对可能是异步、同步的源,即不同类型源的创建,其会对应着不同类型的xxxSubscription,其都会实现QueueSubscription接口。
  2. 由于状态值在多线程情况下的改变可能会导致异常,因此各个子类通过实现对应的方法requestFusion()判断各自的请求拉取策略,判断是同步的还是异步的。从而判断是否支持背压操作。
  3. 可以通过这种催化方式,保证上下链中,将消费控制在一个单线程中,保证接受多个线程同时请求下发元素。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值