Asynchronous artifact: CompletableFuture implementation principle and usage scenarios

1 Overview

CompletableFuture is an implementation class introduced by jdk1.8. Extends Future and CompletionStage, which is a Future that can trigger some operations during the task completion phase. Simply put, it is possible to implement asynchronous callbacks.

2. Why introduce CompletableFuture

For the Future of jdk1.5, although it provides the ability to process tasks asynchronously, the way to get the results is very inelegant, and it still needs to be blocked (or trained in rotation). How to avoid blocking? In fact, it is to register the callback.

The industry implements asynchronous callbacks in combination with the observer pattern. That is, when the task execution is completed, the observer is notified. For example, Netty's ChannelFuture can process asynchronous results by registering listeners.

Netty的ChannelFuture

public Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) {
    checkNotNull(listener, "listener");
    synchronized (this) {
        addListener0(listener);
    }
    if (isDone()) {
        notifyListeners();
    }
    return this;
}
private boolean setValue0(Object objResult) {
    if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
        RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
        if (checkNotifyWaiters()) {
            notifyListeners();
        }
        return true;
    }
    return false;
}

Register listeners through the addListener method. If the task is completed, notifyListeners notification will be called.

CompletableFuture introduces functional programming by extending Future, and processes the result through callback.

3. Function

The function of CompletableFuture is mainly reflected in his CompletionStage.

Can achieve the following functions

  • Convert (thenCompose)
  • thenCombine
  • Consumption (thenAccept)
  • run (thenRun).
  • The difference between consumption and operation with return consumption (thenApply) : consumption uses the execution result. Run simply runs a specific task. You can check other specific functions according to your needs.

CompletableFuture can achieve chained calls with the help of CompletionStage methods. And you can choose synchronous or asynchronous two ways.

Here is a simple example to experience his function.

public static void thenApply() {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    CompletableFuture cf = CompletableFuture.supplyAsync(() -> {
        try {
            //  Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("supplyAsync " + Thread.currentThread().getName());
        return "hello";
    }, executorService).thenApplyAsync(s -> {
        System.out.println(s + "world");
        return "hhh";
    }, executorService);
    cf.thenRunAsync(() -> {
        System.out.println("ddddd");
    });
    cf.thenRun(() -> {
        System.out.println("ddddsd");
    });
    cf.thenRun(() -> {
        System.out.println(Thread.currentThread());
        System.out.println("dddaewdd");
    });
}

Results of the

supplyAsync pool-1-thread-1
helloworld
ddddd
ddddsd
Thread[main,5,main]
dddaewdd

According to the results, we can see that the corresponding tasks will be executed in an orderly manner.

Notice:

If cf.thenRun is executed synchronously. Its execution thread may be the main thread or the thread that executes the source task. If the thread executing the source task finishes executing the task before main is called. Then the cf.thenRun method will be called by the main thread.

Here is an explanation, if there are multiple dependent tasks for the same task:

If these dependent tasks are executed synchronously. Then if these tasks are executed by the current calling thread (main), they are executed in order. If they are executed by the thread that executes the source task, they will be executed in reverse order. Because the internal task data structure is LIFO. If these dependent tasks are executed asynchronously, then he will execute the task through the asynchronous thread pool. The order of execution of tasks is not guaranteed. The above conclusions are obtained by reading the source code. Below we dive into the source code.

3. There are many ways to create a CompletableFuture by source tracking, and you can even create a new one directly. Let's take a look at the method that supplyAsync creates asynchronously.

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                   Executor executor) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}
static Executor screenExecutor(Executor e) {
    if (!useCommonPool && e == ForkJoinPool.commonPool())
        return asyncPool;
    if (e == null) throw new NullPointerException();
    return e;
}

Input parameter Supplier, a function with a return value. If it is an asynchronous method and an executor is passed, then the passed executor will be used to execute the task. Otherwise, the public ForkJoin parallel thread pool is used. If parallelism is not supported, a new thread is created to execute.

Here we need to note that ForkJoin executes tasks through daemon threads. So there must be the existence of non-daemon threads.

asyncSupplyStage method

static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
                                                 Supplier<U> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<U> d = new CompletableFuture<U>();
    e.execute(new AsyncSupply<U>(d, f));
    return d;
}

This will create a CompletableFuture for the return.

Then construct an AsyncSupply and pass the created CompletableFuture as a construction parameter. Then, the execution of the task is completely dependent on AsyncSupply.

AsyncSupply#run

public void run() {
    CompletableFuture<T> d; Supplier<T> f;
    if ((d = dep) != null && (f = fn) != null) {
        dep = null; fn = null;
        if (d.result == null) {
            try {
                d.completeValue(f.get());
            } catch (Throwable ex) {
                d.completeThrowable(ex);
            }
        }
        d.postComplete();
    }
}

1. This method will call Supplier's get method. and set the result into CompletableFuture. We should be aware that these operations are called in asynchronous threads.

The 2.d.postComplete method is to notify the completion of the task execution. Triggering the execution of subsequent dependent tasks is the key to implementing CompletionStage. Before looking at the postComplete method, let's take a look at the logic of creating dependent tasks.

thenAcceptAsync method

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
    return uniAcceptStage(asyncPool, action);
}
private CompletableFuture<Void> uniAcceptStage(Executor e,
                                               Consumer<? super T> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<Void> d = new CompletableFuture<Void>();
    if (e != null || !d.uniAccept(this, f, null)) {
        # 1
        UniAccept<T> c = new UniAccept<T>(e, d, this, f);
        push(c);
        c.tryFire(SYNC);
    }
    return d;
}

mentioned above. thenAcceptAsync is used to consume CompletableFuture. This method calls uniAcceptStage.

uniAcceptStage logic:

1. Construct a CompletableFuture, mainly for chain calls.

2. If it is an asynchronous task, return directly. Because the end of the source task will trigger the asynchronous thread to execute the corresponding logic.

3. If it is a synchronous task (e==null), the d.uniAccept method will be called. The logic of this method is here: if the source task completes, call f and return true. Otherwise go to the if block (Mark 1).

4. If it is an asynchronous task, go directly to if (Mark 1).

Mark1 logic:

1. Construct a UniAccept and push it onto the stack. Optimistic locking is implemented here through CAS.

2. Call the c.tryFire method.

final CompletableFuture<Void> tryFire(int mode) {
    CompletableFuture<Void> d; CompletableFuture<T> a;
    if ((d = dep) == null ||
        !d.uniAccept(a = src, fn, mode > 0 ? null : this))
        return null;
    dep = null; src = null; fn = null;
    return d.postFire(a, mode);
}

1. The d.uniAccept method will be called. In fact, this method judges whether the source task is completed, if it is completed, it executes the dependent task, otherwise it returns false.

2. If the dependent task has been executed, call d.postFire, which is mainly the subsequent processing of Fire. The logic is different according to different modes. Briefly here, in fact, mode has synchronous asynchronous, and iteration. Iterate to avoid infinite recursion.

Emphasize here the third parameter of the d.uniAccept method.

If it is an asynchronous call (mode>0), pass in null. Otherwise pass in this. See the code below for the difference. c is not null will call the c.claim method.

try {
    if (c != null && !c.claim())
        return false;
    @SuppressWarnings("unchecked") S s = (S) r;
    f.accept(s);
    completeNull();
} catch (Throwable ex) {
    completeThrowable(ex);
}

final boolean claim() {
    Executor e = executor;
    if (compareAndSetForkJoinTaskTag((short)0, (short)1)) {
        if (e == null)
            return true;
        executor = null; // disable
        e.execute(this);
    }
    return false;
}

The claim method is logical:

null if async thread. Indicates synchronization, then returns true directly. Finally, the upper-level function will call f.accept(s) to execute the task synchronously. If the async thread is not null, use the async thread to execute this. The run task of this is as follows. That is, the tryFire method is called synchronously in an asynchronous thread. achieve its purpose of being executed by an asynchronous thread.

public final void run()                { tryFire(ASYNC); }

After reading the above logic, we basically understand the logic of dependent tasks.

In fact, it is to first judge whether the source task is completed. If it is completed, execute the previous task directly in the corresponding thread (if it is synchronous, it will be processed in the current thread, otherwise it will be processed in the asynchronous thread).

If the task is not completed, return directly, because after the task is completed, the dependent task will be triggered through postComplete.

postComplete method

final void postComplete() {
    /*
     * On each step, variable f holds current dependents to pop
     * and run.  It is extended along only one path at a time,
     * pushing others to avoid unbounded recursion.
     */
    CompletableFuture<?> f = this; Completion h;
    while ((h = f.stack) != null ||
           (f != this && (h = (f = this).stack) != null)) {
        CompletableFuture<?> d; Completion t;
        if (f.casStack(h, t = h.next)) {
            if (t != null) {
                if (f != this) {
                    pushStack(h);
                    continue;
                }
                h.next = null;    // detach
            }
            f = (d = h.tryFire(NESTED)) == null ? this : d;
        }
    }
}

Called after the source task completes.

In fact, the logic is very simple, it is the dependent task of iterating the stack. Call the h.tryFire method. NESTED is to avoid recursive infinite loops. Because FirePost will call postComplete. Not called if NESTED.

The content of the stack is actually added when the dependent task is created. We have mentioned above.

4. Summary

Basically the above source code has analyzed the logic.

Because it involves asynchronous operations, we need to take care of it (here for fully asynchronous tasks):

1. After the CompletableFuture is successfully created, the corresponding task will be executed through an asynchronous thread.

2. If CompletableFuture has dependent tasks (asynchronous), the tasks will be added to the stack of CompletableFuture and saved. For subsequent execution of dependent tasks after completion.

Of course, creating a dependent task doesn't just add it to the stack. If the source task has been executed when the dependent task is created, the current thread will trigger the asynchronous thread of the dependent task to directly process the dependent task. And it will tell the stack that other dependent task source tasks have completed.

Mainly consider the reuse of code. So the logic is relatively difficult to understand.

The postComplete method will be called by the source task thread after executing the source task. The same may also be called after the thread of the dependent task.

The main way to perform dependent tasks is to rely on the tryFire method. Because this method may be triggered by many different types of threads, the logic is also detoured a bit. (other dependent task threads, source task threads, current dependent task threads)

If it is the current dependent task thread, the dependent task will be executed and other dependent tasks will be notified. If it is the source task thread and other dependent task threads, the task is converted to the dependent thread for execution. No need to notify other dependent tasks, avoiding dead recursion.

I have to say that Doug Lea's coding is really art. The reusability of the code is now logical.

Link: https://blog.csdn.net/weixin_39332800/article/details/108185931

Finally, I will share with you a few good github projects that I have collected. The content is still good. If you find it helpful, you can give a star by the way.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324104835&siteId=291194637