The source code of this article is based on JDK13
Flow
Official annotation translation
For some interfaces and static methods, in order to establish streaming components, Publisher generates elements, which are consumed by one or more subscribers, and each Subscriber is managed by Subscription
Interface introduction: reactive-streams They are suitable for concurrent and distributed environments. All methods are defined as Wu Xiao's one-way message style
Communication depends on a simple form of flow control. It can be used to avoid resource management problems in push type systems
Example:
A Flow.Publisher usually defines its own Subscription implementation, creates one in the subscribe method, and then asks it to give it to Flow.Subscriber.
Bucket asynchronous message publishing usually uses a thread pool. The following is a simple publisher, which only publishes a TRUE to a single subscriber. Because the subscriber receives only a simple element, this class does not need to use buffering and sequence control
class OneShotPublisher implements Publisher<Boolean> { //Thread pool private final ExecutorService executor = ForkJoinPool.commonPool(); // daemon-based // Whether to be subscribed, because this publisher can only be subscribed by one person private boolean subscribed; // true after first subscribe // Subscription method public synchronized void subscribe(Subscriber<? super Boolean> subscriber) { if (subscribed) subscriber.onError(new IllegalStateException()); // only one allowed else { // Subscription succeeded subscribed = true; subscriber.onSubscribe(new OneShotSubscription(subscriber, executor)); } } // subscription management static class OneShotSubscription implements Subscription { // subscriber private final Subscriber<? super Boolean> subscriber; // Thread pool private final ExecutorService executor; // result private Future<?> future; // to allow cancellation // Complete private boolean completed; // Construction method OneShotSubscription(Subscriber<? super Boolean> subscriber, ExecutorService executor) { this.subscriber = subscriber; this.executor = executor; } // request public synchronized void request(long n) { // Not completed if (!completed) { completed = true; if (n <= 0) { IllegalArgumentException ex = new IllegalArgumentException(); executor.execute(() -> subscriber.onError(ex)); } else { // Execution method future = executor.submit(() -> { subscriber.onNext(Boolean.TRUE); subscriber.onComplete(); }); } } } // cancel public synchronized void cancel() { completed = true; if (future != null) future.cancel(false); } } }
This is a very simple application scenario. A single publisher publishes messages to a single consumer
A Flow.Subscriber arranges the request and processing of elements. Elements will not be published before calling request, but multiple elements may be requested
Many Subscriber implementations can manage elements according to the following style. The buffer size is usually 1 single step. Larger buffer size usually allows more efficient overlapping processing and less communication at the same time
For example, given a number of 64, the total number of outstanding requests will remain between 32 and 64. Because the calls of Subscriber methods are strictly ordered, these methods do not need to use locks or volatile unless the Subscriber maintains multiple subscriptions
class SampleSubscriber<T> implements Subscriber<T> { final Consumer<? super T> consumer; Subscription subscription; final long bufferSize; long count; SampleSubscriber(long bufferSize, Consumer<? super T> consumer) { this.bufferSize = bufferSize; this.consumer = consumer; } public void onSubscribe(Subscription subscription) { long initialRequestSize = bufferSize; count = bufferSize - bufferSize / 2; // re-request when half consumed (this.subscription = subscription).request(initialRequestSize); } public void onNext(T item) { if (--count <= 0) subscription.request(count = bufferSize - bufferSize / 2); consumer.accept(item); } public void onError(Throwable ex) { ex.printStackTrace(); } public void onComplete() {} }
The default value of defaultBufferSize usually provides a useful seven points for selecting the request size and capacity in the Flow component according to the expected rate and resource usage. Alternatively, when Flow control is not required, subscribers can initialize an unbounded queue collection
class UnboundedSubscriber<T> implements Subscriber<T> { public void onSubscribe(Subscription subscription) { subscription.request(Long.MAX_VALUE); // effectively unbounded } public void onNext(T item) { use(item); } public void onError(Throwable ex) { ex.printStackTrace(); } public void onComplete() {} void use(T item) { ... } }
Source code
Publisher publisher
public static interface Publisher<T> { public void subscribe(Subscriber<? super T> subscriber); }
Defines adding a subscriber to Publisher
Subscriber subscriber
public static interface Subscriber<T> { public void onSubscribe(Subscription subscription); public void onNext(T item); public void onError(Throwable throwable); public void onComplete(); }
The subscriber's interface defines:
- onSubscribe adding a subscription TODO is incorrect
- onNext handles an element
- onError error
- onComplete complete
Subscription subscription
A message manager that links between publishers and subscribers
public static interface Subscription { /** * Adds the given number {@code n} of items to the current * unfulfilled demand for this subscription. If {@code n} is * less than or equal to zero, the Subscriber will receive an * {@code onError} signal with an {@link * IllegalArgumentException} argument. Otherwise, the * Subscriber will receive up to {@code n} additional {@code * onNext} invocations (or fewer if terminated). * * @param n the increment of demand; a value of {@code * Long.MAX_VALUE} may be considered as effectively unbounded */ public void request(long n); /** * Causes the Subscriber to (eventually) stop receiving * messages. Implementation is best-effort -- additional * messages may be received after invoking this method. * A cancelled subscription need not ever receive an * {@code onComplete} or {@code onError} signal. */ public void cancel(); }
- request adds a given number of elements
- Cancel cancel
Processor
At the same time, it implements a component class of producer and consumer ~
SubmissionPublisher
Official annotation translation
A Flow.Publisher asynchronously submits non empty elements to its subscribers until they are closed. Each subscriber accepts newly submitted elements in the same order. Unless exceptions are encountered. SubmissionPublisher allows element generation to be compatible with reactive streams. The publisher relies on dop or blocking for flow control
Submission publisher uses a thread pool to submit to its subscribers. The thread pool is selected according to its usage field
If the submitted element runs in a separate thread and the number of subscribers can be estimated, you can use Executors.newFixedThreadPool. Otherwise, ForkJoinPoll.commonPool. Is used by default
Buffers allow producers and consumers to temporarily run at different rates. Each subscriber uses a separate buffer. The buffer is rebuilt on first use and expanded as needed
The call of request does not directly lead to the expansion of the buffer. However, if the filled request exceeds the maximum capacity, there is a risk of saturation. Flow.defaultBufferSize provides seven points of capacity, based on the expected speed, resources and usage
The publish method supports different strategies for buffer saturation. The submit code block knows that resources are available. This is the simplest strategy, but the slowest. The offer method may discard elements, but provides an opportunity to insert processing and retry
If some subscriber's methods throw exceptions, their subscription will be cancelled. If a handler is submitted in the constructor method, the onNext method will call the processing method if an exception occurs, but the onSubscribeOnError and OnComplete methods do not record and handle exceptions
If RejectedExecutionException or other runtime exceptions occur when submitting to the thread pool, or an exception is thrown by a discard processor. Not all subscribers can receive the published elements
The consume method simplifies support for common situations where the subscriber's only action is to request and process all items using the provided function
This class can also serve as a basis for subclasses of generated items and use the methods in this class to publish them. For example:
Here is a class that periodically publishes publishing elements. (in fact, you can add methods to start and stop independently, share thread pools among publishers, etc., or use SubmissionPublisher as a component instead of a superclass.)
class PeriodicPublisher<T> extends SubmissionPublisher<T> { // Periodic task final ScheduledFuture<?> periodicTask; // Thread pool final ScheduledExecutorService scheduler; PeriodicPublisher(Executor executor, int maxBufferCapacity, Supplier<? extends T> supplier, long period, TimeUnit unit) { super(executor, maxBufferCapacity); scheduler = new ScheduledThreadPoolExecutor(1); periodicTask = scheduler.scheduleAtFixedRate( () -> submit(supplier.get()), 0, period, unit); } public void close() { periodicTask.cancel(false); scheduler.shutdown(); super.close(); } }
Here is an implementation example of Flow.Processor. It uses one-step request to its publisher. The more adaptable version can use the delay of submission and return and other methods to monitor the flow
class TransformProcessor<S,T> extends SubmissionPublisher<T> implements Flow.Processor<S,T> { final Function<? super S, ? extends T> function; Flow.Subscription subscription; TransformProcessor(Executor executor, int maxBufferCapacity, Function<? super S, ? extends T> function) { super(executor, maxBufferCapacity); this.function = function; } public void onSubscribe(Flow.Subscription subscription) { (this.subscription = subscription).request(1); } public void onNext(S item) { subscription.request(1); submit(function.apply(item)); } public void onError(Throwable ex) { closeExceptionally(ex); } public void onComplete() { close(); } }
It's hard to understand... After translation, it became more difficult to understand
I highly recommend this article. I have read it clearly:
Source code introduction
SubmissionPublisher publisher feature
This class is also the outermost class
attribute
// Linked list of subscribers BufferedSubscription<T> clients; // Is it closed volatile boolean closed; // Exception causing shutdown volatile Throwable closedException; // Thread pool final Executor executor; // handler processor final BiConsumer<? super Subscriber<? super T>, ? super Throwable> onNextHandler; // Maximum buffer capacity final int maxBufferCapacity;
A publisher can be subscribed by multiple subscribers. These subscribers use a linked list to save. In addition, some states of the current publisher are recorded, which are detailed in the comments
Construction method
public SubmissionPublisher(Executor executor, int maxBufferCapacity, BiConsumer<? super Subscriber<? super T>, ? super Throwable> handler) { if (executor == null) throw new NullPointerException(); if (maxBufferCapacity <= 0) throw new IllegalArgumentException("capacity must be positive"); this.executor = executor; this.onNextHandler = handler; this.maxBufferCapacity = roundCapacity(maxBufferCapacity); } public SubmissionPublisher(Executor executor, int maxBufferCapacity) { this(executor, maxBufferCapacity, null); } public SubmissionPublisher() { this(ASYNC_POOL, Flow.defaultBufferSize(), null); }
Assign value after parameter verification
subscribe subscription method
This is the implementation method as the publisher interface
public void subscribe(Subscriber<? super T> subscriber) { if (subscriber == null) throw new NullPointerException(); int max = maxBufferCapacity; // allocate initial array Object[] array = new Object[max < INITIAL_CAPACITY ? max : INITIAL_CAPACITY]; // Create subscription token BufferedSubscription<T> subscription = new BufferedSubscription<T>(subscriber, executor, onNextHandler, array, max); // Lock execution synchronized (this) { // The thread that records the first subscriber if (!subscribed) { subscribed = true; owner = Thread.currentThread(); } for (BufferedSubscription<T> b = clients, pred = null;;) { // The current subscriber is the first if (b == null) { Throwable ex; subscription.onSubscribe(); if ((ex = closedException) != null) subscription.onError(ex); else if (closed) subscription.onComplete(); else if (pred == null) clients = subscription; else pred.next = subscription; break; } // Link to back BufferedSubscription<T> next = b.next; if (b.isClosed()) { // remove b.next = null; // detach if (pred == null) clients = next; else pred.next = next; } else if (subscriber.equals(b.subscriber)) { b.onError(new IllegalStateException("Duplicate subscribe")); break; } else pred = b; b = next; } } }
- First, construct a subscription token based on the current subscriber
- Find the tail of the linked list and insert the current subscriber
- After that, we call the OnSubscribe method of the subscription token. We will look at the code of the subscriber and token later.
The submit submission element is published by the publisher
public int submit(T item) { return doOffer(item, Long.MAX_VALUE, null); } private int doOffer(T item, long nanos, BiPredicate<Subscriber<? super T>, ? super T> onDrop) { if (item == null) throw new NullPointerException(); int lag = 0; boolean complete, unowned; synchronized (this) { Thread t = Thread.currentThread(), o; BufferedSubscription<T> b = clients; if ((unowned = ((o = owner) != t)) && o != null) owner = null; // disable bias if (b == null) complete = closed; else { complete = false; boolean cleanMe = false; BufferedSubscription<T> retries = null, rtail = null, next; // Circularly call the offer method of the token to publish the message do { next = b.next; int stat = b.offer(item, unowned); if (stat == 0) { // saturated; add to retry list b.nextRetry = null; // avoid garbage on exceptions if (rtail == null) retries = b; else rtail.nextRetry = b; rtail = b; } else if (stat < 0) // closed cleanMe = true; // remove later else if (stat > lag) lag = stat; } while ((b = next) != null); if (retries != null || cleanMe) lag = retryOffer(item, nanos, onDrop, retries, lag, cleanMe); } } if (complete) throw new IllegalStateException("Closed"); else return lag; }
ConsumerSubscriber subscriber implementation
static final class ConsumerSubscriber<T> implements Subscriber<T> { final CompletableFuture<Void> status; final Consumer<? super T> consumer; Subscription subscription; // Save the Consumer, status, and token ConsumerSubscriber(CompletableFuture<Void> status, Consumer<? super T> consumer) { this.status = status; this.consumer = consumer; } // The issuer sends the token back to the subscriber public final void onSubscribe(Subscription subscription) { this.subscription = subscription; status.whenComplete((v, e) -> subscription.cancel()); if (!status.isDone()) subscription.request(Long.MAX_VALUE); } // error handling public final void onError(Throwable ex) { status.completeExceptionally(ex); } // complete public final void onComplete() { status.complete(null); } // Process the next element, Consumer execution public final void onNext(T item) { try { consumer.accept(item); } catch (Throwable ex) { subscription.cancel(); status.completeExceptionally(ex); } } }
This class is relatively simple, because no specific business implementation is implemented, it only accepts the token, handles the error, completes, and receives the publisher's message every time, then calls the initialization Consumer to consume. "
Implementation of BufferedSubscription subscription token
long timeout; // Long.MAX_VALUE if untimed wait int head; // next position to take int tail; // next position to put final int maxCapacity; // max buffer size volatile int ctl; // atomic run state flags Object[] array; // buffer final Subscriber<? super T> subscriber; final BiConsumer<? super Subscriber<? super T>, ? super Throwable> onNextHandler; Executor executor; // null on error Thread waiter; // blocked producer thread Throwable pendingError; // holds until onError issued BufferedSubscription<T> next; // used only by publisher BufferedSubscription<T> nextRetry; // used only by publisher @jdk.internal.vm.annotation.Contended("c") // segregate volatile long demand; // # unfilled requests @jdk.internal.vm.annotation.Contended("c") volatile int waiting; // nonzero if producer blocked // ctl bit values static final int CLOSED = 0x01; // if set, other bits ignored static final int ACTIVE = 0x02; // keep-alive for consumer task static final int REQS = 0x04; // (possibly) nonzero demand static final int ERROR = 0x08; // issues onError when noticed static final int COMPLETE = 0x10; // issues onComplete when done static final int RUN = 0x20; // task is or will be running static final int OPEN = 0x40; // true after subscribe static final long INTERRUPTED = -1L; // timeout vs interrupt sentinel
This is actually the subscription implementation saved in the publisher, which is a linked list node
- array saves the messages in the current subscription token
- Next implements the next node pointer of the linked list node
offer accept message
In the publisher, the message is published through the offer of the internal linked list node, which is here
// Writes elements to an array final int offer(T item, boolean unowned) { Object[] a; int stat = 0, cap = ((a = array) == null) ? 0 : a.length; int t = tail, i = t & (cap - 1), n = t + 1 - head; if (cap > 0) { boolean added; if (n >= cap && cap < maxCapacity) // resize added = growAndOffer(item, a, t); else if (n >= cap || unowned) // need volatile CAS added = QA.compareAndSet(a, i, null, item); else { // can use release mode QA.setRelease(a, i, item); added = true; } if (added) { tail = t + 1; stat = n; } } return startOnOffer(stat); } // After the element joins the team, try to start a task consumption final int startOnOffer(int stat) { int c; // start or keep alive if requests exist and not active if (((c = ctl) & (REQS | ACTIVE)) == REQS && ((c = getAndBitwiseOrCtl(RUN | ACTIVE)) & (RUN | CLOSED)) == 0) tryStart(); else if ((c & CLOSED) != 0) stat = -1; return stat; } // Try to start a task and call the current consumer method final void tryStart() { try { Executor e; ConsumerTask<T> task = new ConsumerTask<T>(this); if ((e = executor) != null) // skip if disabled on error e.execute(task); } catch (RuntimeException | Error ex) { getAndBitwiseOrCtl(ERROR | CLOSED); throw ex; } }
summary
It's complicated. I didn't look at the code carefully. I just need to know the general implementation
SubmissionPublisher implements the interface defined in the Flow class and provides a set of responsive API s. Its call chain is about:
Note that all operations are asynchronous
- Subscriber registers himself with Publisher and calls Publisher.subscribe()
- Publisher accepts registration, generates a token, returns it to Subscriber, and calls Subscriber.onSubscribe()
- The Subscriber tells Publisher how many messages he needs through the token Subscription.request() (note that this step can tell the maximum value at one time or in batches)
- The program publishes a message through Publisher.submit(). Publisher calls their offer methods one by one through the internally saved Subscription linked list. The number of messages required by each subscriber needs to be considered
- Subscription starts the task according to its strategy, whether to buffer or not, and invokes the Subscriber.onNext execution method in the task.
Reference articles
End.
Contact me
Finally, welcome to my personal official account, Yan Yan ten, which will update many learning notes from the backend engineers. I also welcome direct official account or personal mail or email to contact me.
The above are all personal thoughts. If there are any mistakes, please correct them in the comment area.
Welcome to reprint, please sign and keep the original link.
Contact email: huyanshi2580@gmail.com
For more study notes, see personal blog or WeChat official account, Yan Yan ten > > Huyan ten