Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景

Java异步编程源码及原理解析

目录

1.Fork-Join
2.FutureTask
3.Async注解介绍
4.CompletableFuture
5.中间件+线程池

会当凌绝顶,一览众山小。

1.Fork-Join框架介绍

从一道面试题讲起:如何使用多线程实现归并排序?

public class Solution {
    private static final ForkJoinPool pool = new ForkJoinPool(4); // N 核心处理器

    // 多线程排序
    private static void multiThreadSort(int[] a) {
        SortTask task = new SortTask(a, 0, a.length - 1);
        pool.invoke(task);
    }

    /**
     * fork-join 框架实现归并排序
     * 这里是分割
     */
    private static class SortTask extends RecursiveTask<int[]> {
        private int[] val;
        private int start;
        private int end;
        private int middle;

        public SortTask(int[] a, int start, int end) {
            val = a;
            this.start = start;
            this.end = end;
            middle = (start + end) / 2;//应写成start+((end-start)>>1)
            if (merge == null) merge = new int[a.length];
        }

        @Override
        protected int[] compute() {
            List<SortTask> moreActions = getMoreActions();
            if (moreActions != null && moreActions.size() > 0) {
                // 执行所有子线程
                for (SortTask t : moreActions) t.fork();
                // 等待所有子线程完成
                for (SortTask t : moreActions) t.join();
                // 合并
                merge(val, start, end, middle);
            } else {
                if (end - start == 1 && val[start] > val[end]) swap(start, end);
            }
            return val;
        }

        // 获取需要分割的其他桶任务
        private List<SortTask> getMoreActions() {
            if (end - start <= 1) return null;
            List<SortTask> moreActions = new ArrayList<>();
            moreActions.add(new SortTask(val, start, middle));
            moreActions.add(new SortTask(val, middle + 1, end));
            return moreActions;
        }

        private void swap(int i, int j) {
            int a = val[i];
            val[i] = val[j];
            val[j] = a;
        }
    }
}

分别进行100万个数字排序,对各个算法都进行200次排序,可以看到,输出结果:

  • 正常排序共耗时:35.43 秒
  • 多线程排序共耗时:23.898 秒

但是如果我们把排序次数改为5次:

  • 正常排序共耗时:1.616 秒
  • 多线程排序共耗时:2.905 秒

使用Fork-Join和串行归并排序比较

  • 在大量的作业的情况下,ForkJoin排序效率更高
  • 在少量作业的情况下,多线程会带来一些额外的开销,比如线程创建等

Fork-Join框架介绍

  • Java 1.7 引入了一种新的并发框架
  • 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数
  • 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个相互独立的小任务,最终汇总每个小任务结果后得到大任务结果的框架
  • 与其他ThreadPool共存

Fork-Join常用的用途

newWorkStealingPool抢占式线程池

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第1张图片

  • newWorkStealingPool会更加所需的并行层次来动态创建和关闭线程。它同样会试图减少任务队列的大小,所以比较适于高负载的环境。同样也比较适用于当执行的任务会创建更多任务,如递归任务。

  • 适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中。

newWorkStealingPool抢占式线程池核心源码分析

public class Executors {

    /**
     创建一个线程池,以维护足够的线程来支持给定的并行级别,可以使用多个队列减少争用。
     并行度级别对应于活动参与的或可用的最大线程数参与任务处理。线程的实际数量可能动态
     增长和收缩。newWorkStealingPool没有保证提交任务的顺序执行。
     *
     * @param parallelism 目标并行度级别
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code parallelism <= 0}
     * @since 1.8
     */
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool(parallelism,
                ForkJoinPool.defaultForkJoinWorkerThreadFactory,
                null, true);
    }

    /**
     * Creates a work-stealing thread pool using all
     * {@link Runtime#availableProcessors available processors}
     * as its target parallelism level.
     * @return the newly created thread pool
     * @see #newWorkStealingPool(int)
     * @since 1.8
     */
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
                ForkJoinPool.defaultForkJoinWorkerThreadFactory,
                null, true);
    }
}
Java 8 parallelStream并行流

并行流就是执行任务的时候分配给多个线程队列执行,但是如果某一个队列执行完成,其他队列还在执行,这个时候执行完成的队列就是空闲状态。

java8中的并行流,使用的是工作窃取模式,在一个队列的任务执行完成之后,他会去其他没有执行完成的任务队列里面窃取尾部的任务来执行。

简单地使用并行流:

import java.util.Arrays;
import java.util.List;

public class Solution {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        numbers.parallelStream().forEach(System.out::println);//结果是乱序的
    }
}

使用并行流并打印线程名:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第2张图片

可以看到并行流使用Main线程和各个ForkJoinPool的工作线程并行执行。

Java 8 CompleteableFuture

后文会讲

Fork-Join的原理

  • 1、ForkJoinPool中的所有的工作线程均有一个自己的工作队列WorkQueue
    • 双端队列(Deque)
    • 从队头取任务
    • 线程私有,不共享
  • 2、ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头
    • 工作线程以LIFO的顺序来处理它队列中的任务
  • 3、为了最大化CPU利用率,空闲的线程将从其他线程的队列中“窃取”任务来执行
    • 从工作队列的队尾“窃取”任务,以减少竞争
    • 任务的“窃取”是以FIFO顺序进行的,因为先放入的任务往往表示更大的工作量
    • 支持“窃取”线程进行进一步的递归分解
  • 3、WorkQueue双端队列最小化任务“窃取”的竞争
    • push()/pop()仅在其所有者工作线程中调用,这些操作都是通过CAS来实现的,是Wait-free的
    • poll() 则由其他工作线程来调用“窃取”任务,可能不是wait-free

任务窃取流程如下:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第3张图片
Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第4张图片
当WorkQueue为空时抢任务:Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第5张图片

一张图表示工作窃取模型:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第6张图片

Fork-Join的核心源码

参考我之前写的PPT:《Fork/join框架与CompleteableFuture源码解析》

后面再写

2.FutureTask

FutureTask提供了对Future的基本实现,可以调用方法去开始和取消一个计算,可以查询计算是否完成并且获取计算结果。

简单地介绍Future接口

Future的类图:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第7张图片

RunnableFuture

这个接口同时继承Future接口和Runnable接口,在成功执行run()方法后,可以通过Future访问执行结果。这个接口都实现类是FutureTask,一个可取消的异步计算,这个类提供了Future的基本实现,后面我们的demo也是用这个类实现,它实现了启动和取消一个计算,查询这个计算是否已完成,恢复计算结果。计算的结果只能在计算已经完成的情况下恢复。如果计算没有完成,get方法会阻塞,一旦计算完成,这个计算将不能被重启和取消,除非调用runAndReset方法。

FutureTask能用来包装一个Callable或Runnable对象,因为它实现了Runnable接口,而且它能被传递到Executor进行执行。为了提供单例类,这个类在创建自定义的工作类时提供了protected构造函数。

SchedualFuture

这个接口表示一个延时的行为可以被取消。通常一个安排好的future是定时任务SchedualedExecutorService的结果

CompleteFuture

一个Future类是显示的完成,而且能被用作一个完成等级,通过它的完成触发支持的依赖函数和行为。当两个或多个线程要执行完成或取消操作时,只有一个能够成功。

ForkJoinTask

基于任务的抽象类,可以通过ForkJoinPool来执行。一个ForkJoinTask是类似于线程实体,但是相对于线程实体是轻量级的。大量的任务和子任务会被ForkJoinPool池中的真实线程挂起来,以某些使用限制为代价。

简单地使用FutureTask

配合线程池使用
public class FutureTest {
    //(1)线程池
    private final static ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.MINUTES,
            new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Future future1 = executorService.submit(() -> System.out.println("start runable one " + Thread.currentThread().getName()));
        Future future2 = executorService.submit(() -> System.out.println("start runable two " + Thread.currentThread().getName()));
        Future future3 = executorService.submit(() -> System.out.println("start runable three " + Thread.currentThread().getName()));
        Future<Integer> future4 = executorService.submit(() -> {
            System.out.println("start runable four " + Thread.currentThread().getName());
            return 1;
        });
        System.out.println("task one res:" + future1.get() + Thread.currentThread().getName());//(5)等待任务one执行完毕
        System.out.println("task two res:" + future2.get() + Thread.currentThread().getName());//(6)等待任务two执行完毕
        System.out.println("task three res:" + future3.get() + Thread.currentThread().getName());// (7)等待任务three执行完毕
        System.out.println("task four res:" + future4.get() + Thread.currentThread().getName());// (8)等待任务four执行完毕
        executorService.shutdown();//(9)关闭线程池,阻塞直到所有任务执行完毕
    }
}

运行结果:

start runable one pool-1-thread-1
start runable two pool-1-thread-2
start runable three pool-1-thread-3
task one res:nullmain
start runable four pool-1-thread-1
task two res:nullmain
task three res:nullmain
task four res:1main
单独使用

FutureTask的核心方法如下:

public class FutureTask<V> implements RunnableFuture<V> {
    // 获取结果,如果计算未完成,当前线程会被阻塞
    public V get() throws InterruptedException, ExecutionException {...}
    // 获取结果,如果计算未完成,当前线程会被阻塞,但是有时间限制
    // 如果超时还未完成,抛出 TimeoutException
    public V get(long timeout, TimeUnit unit){...}
    // 进行计算,运行 Callable 的 call 方法,然后对结果进行赋值
    public void run(){...}
    // 进行计算,运行 Callable 的 call 方法,然后将 state 重置为 NEW
    // 不对结果进行赋值,可以重复多次运行,只有子类可以调用,用来进行扩展,例如 ScheduledFutureTask
    protected void runAndReset(){...}
    // 取消任务,参数 mayInterruptIfRunning 表示取消过程中是否中断当前线程
    public boolean cancel(boolean mayInterruptIfRunning){...}
    // 查看任务是否被取消
    public boolean isCancelled(){...}
    // 查看任务是否完成
    public boolean isDone(){...}
}

具体的使用方式请百度看其他的文章

优缺点

优点

  • 1、可配合线程池使用,也可以单独创建,适合使用在经典的线程池中。
  • 2、适合在JDK1.5~1.7的版本中,因为CompletableFuturn在JDK1.8的版本中才出现。

缺点

  • 1、用法比CompletableFuture复杂。
  • 2、执行效率不如CompletableFuture。

CompletableFuture是何方神圣?后文说明。

请忘掉FutureTask!

3.Async注解

对于异步方法调用,从Spring3开始提供了@Async注解,该注解可以被标在方法上,以便异步地调用该方法。调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行。

Spring 已经实现的线程池

  • SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
  • SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
  • ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
  • SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
  • ThreadPoolTaskExecutor :最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

异步的方法

  • 最简单的异步调用,返回值为void
  • 带参数的异步调用,异步方法可以传入参数
  • 存在返回值,常调用返回Future

使用@Async的步骤

1、Java配置

@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }

2、使用@Async注解标注方法

//基于@Async无返回值调用
@Async  
public void asyncMethodWithVoidReturnType() {...}
//基于@Async返回值的调用
@Async
public Future<T> asyncMethodWithReturnType() {...}

是不是感觉太复杂了?

优缺点

  • 1、依赖于Spring(侵入式)。
  • 2、用法比较复杂。

4.CompletableFuture

先介绍Java8的函数式编程

四种常用的函数介绍

  • (1)Function => R apply(T t) ———— 接受一个T类型的参数,返回R类型结果。
Function<Integer, String> function = (x) -> "result: " + x;
System.out.println(function.apply(6));
  • (2)Consumer => void accept(T t) ———— 接受一个T类型的参数,无返回。
Consumer<String> consumer = (x) -> System.out.println("consumer: " + x);
consumer.accept("Hello");
//见过System.out::println没?
  • (3)Predicate => boolean test(T t) ———— 接受一个T类型的参数,返回布尔值。
Predicate<String> predicate = (x) -> x.length() > 0;
System.out.println(predicate.test("String"));
  • (4)Supplier => T get() ———— 无输入参数,返回T类型的一个结果。
Supplier<String> supplier = () -> "Test supplier";
System.out.println(supplier.get());

在CompleteableFuture中,有大量这样类型的传参

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第8张图片

CompletableFuture的核心API介绍

supplyAsync / runAsync

supplyAsync表示创建带返回值的异步任务的,相当于ExecutorService submit(Callable task) 方法,runAsync表示创建无返回值的异步任务,相当于ExecutorService submit(Runnable task)方法,这两方法的效果跟submit是一样的。

supplyAsync的用法

// 创建异步执行任务,有返回值
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
    System.out.println("future1:" + Thread.currentThread().getName());
    return 3.14159;
});
//等待子任务执行完成
Double res = future1.get();
System.out.println("run result->" + res + " 当前线程:" + Thread.currentThread().getName());

打印结果:

future1:ForkJoinPool.commonPool-worker-1
run result->3.14159 当前线程:main

runAsync的用法

// 创建异步执行任务,无返回值
CompletableFuture.runAsync(() -> {
    System.out.println("future2:" + Thread.currentThread().getName());
    System.out.println("hello world");
});

打印结果:

future2:ForkJoinPool.commonPool-worker-1
hello world

这两方法各有一个重载版本,可以指定执行异步任务的Executor实现,如果不指定,默认使用ForkJoinPool.commonPool(),如果机器是单核的,则默认使用ThreadPerTaskExecutor,该类是一个内部类,每次执行execute都会创建一个新线程。

ForkJoinPool pool = new ForkJoinPool();
ExecutorService executorService = Executors.newSingleThreadExecutor();
CompletableFuture.supplyAsync(() -> 3.1415926, pool);
CompletableFuture.runAsync(() -> System.out.println("hello"), executorService);
异步回调thenApply / thenApplyAsync

thenApply 表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中。

thenApplyAsync与thenApply的区别在于,前者是将job2提交到线程池中异步执行,实际执行job2的线程可能是另外一个线程,后者是由执行job1的线程立即执行job2,即两个job都是同一个线程执行的。

ForkJoinPool pool = new ForkJoinPool();
// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName());
    return 3.1415926;
}, pool);
//future1关联的异步任务的返回值作为方法入参,传入到thenApply的方法中
//thenApply这里实际创建了一个新的CompletableFuture实例
CompletableFuture<String> future2 = future1.thenApply((result) -> {
    System.out.println(Thread.currentThread().getName());
    return "半径为3的圆的周长:" + 3 * 2 * result;
});
CompletableFuture<Double> future3 = future1.thenApplyAsync((result) -> {
    System.out.println(Thread.currentThread().getName());
    return 3 * 3 * result;
});
//等待子任务执行完成
System.out.println(Thread.currentThread().getName() + " " + future2.get());//这里不考虑计算结果精度,暂时不用BigDecimal
System.out.println(Thread.currentThread().getName() + " 面积:" + future3.get());

打印结果:

ForkJoinPool-1-worker-1
main
main run result->半径为3的圆的周长:18.849555600000002
thenAccept / thenRun

thenAccept 同 thenApply 接收上一个任务的返回值作为参数,但是无返回值;thenRun 的方法没有入参,也没有返回值。

ForkJoinPool pool = new ForkJoinPool();
// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
    System.out.println(Thread.currentThread().getName());
    return 3.1415926;
}, pool);
//future1关联的异步任务的返回值作为方法入参,传入到thenApply的方法中
CompletableFuture<Void> future2 = future1.thenApply((result) -> {
    System.out.println(Thread.currentThread().getName() + " 旧的res:" + result);
    return result + 1;
    //接收上一个任务的执行结果作为入参,但是没有返回值
}).thenAccept(result2 -> {
    System.out.println(Thread.currentThread().getName() + " 新的res:" + result2);
}).thenRun(() -> { //无入参,也没有返回值
    System.out.println(Thread.currentThread().getName() + " hello ");
});
//等待子任务执行完成
System.out.println("run result->" + future1.get());
//future2 等待最后一个thenRun执行完成
System.out.println("run result->" + future2.get());

打印结果:

ForkJoinPool-1-worker-1
main 旧的res:3.1415926
main 新的res:4.1415926
main hello 
run result->3.1415926
run result->null
exceptionally异常处理

exceptionally方法指定某个任务执行异常时执行的回调方法,会将抛出异常作为参数传递到回调方法中,如果该任务正常执行则会exceptionally方法返回的CompletionStage的result就是该任务正常执行的结果。

double pi = 3.1415926;
ForkJoinPool pool = new ForkJoinPool();
// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
    System.out.println("future1当前线程名:" + Thread.currentThread().getName());
    System.out.println(1 / 0);
    return pi;
}, pool);
//future1执行异常时,将抛出的异常作为入参传递给回调方法
CompletableFuture<Double> future2 = future1.exceptionally(e -> {
    System.out.println("future2当前线程名:" + Thread.currentThread().getName());
    e.printStackTrace();
    return pi + 1;
});
//future1正常执行时执行的逻辑,如果执行异常则不调用此逻辑
future1.thenAccept((param) -> {
    System.out.println("future3当前线程名:" + Thread.currentThread().getName());
    System.out.println("半径为3的圆的周长" + param * 3 * 2);
});
//等待子任务执行完成,此处无论是job2和job3都可以实现job2退出,主线程才退出,如果是future1,则主线程不会等待job2执行完成自动退出了
//future2.get时,没有异常,但是依然有返回值,就是cf的返回值
System.out.println("run result->" + future2.get());

当出异常时,打印结果:

future1当前线程名:ForkJoinPool-1-worker-1
future2当前线程名:main
run result->4.1415926
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero

无异常时,打印结果:

future1当前线程名:ForkJoinPool-1-worker-1
future3当前线程名:main
半径为3的圆的周长18.849555600000002
run result->3.1415926
其他API

此外,还有whenComplete、handle、组合处理thenCombine / thenAcceptBoth / runAfterBoth、applyToEither / acceptEither / runAfterEither、thenCompose等很好用的API,在此不做详细说明。

allOf / anyOf

allOf返回的CompletableFuture是多个任务都执行完成后才会执行,只有有一个任务执行异常,则返回的CompletableFuture执行get方法时会抛出异常,如果都是正常执行,则get返回null。

anyOf返回的CompletableFuture是多个任务只要其中一个执行完成就会执行,其get返回的是已经执行完成的任务的执行结果,如果该任务执行异常,则抛出异常。

// 创建异步执行任务:
CompletableFuture<Double> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("future1当前线程名:" + Thread.currentThread().getName());
return 0.02;
});
CompletableFuture<Double> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("future2当前线程名:" + Thread.currentThread().getName());
return 0.03;
});
CompletableFuture<Double> future3 = CompletableFuture.supplyAsync(() -> {
System.out.println("future3当前线程名:" + Thread.currentThread().getName());
System.out.println(1 / 0);
return 0.04;
});
CompletableFuture<Double> future4 = CompletableFuture.supplyAsync(() -> {
System.out.println("future4当前线程名:" + Thread.currentThread().getName());
return 0.05;
});

//allof等待所有任务执行完成才执行future5,如果有一个任务异常终止,则cf4.get时会抛出异常,都是正常执行,cf4.get返回null
CompletableFuture.allOf(future1, future2, future3, future4).whenComplete((a, b) -> {
if (b != null) {
b.printStackTrace();
} else {
System.out.println("allof->" + a);
}
});
//anyOf是只有一个任务执行完成,无论是正常执行或者执行异常,都会执行cf4,cf4.get的结果就是已执行完成的任务的执行结果
CompletableFuture.anyOf(future1, future2, future3, future4).whenComplete((a, b) -> {
if (b != null) {
b.printStackTrace();
} else {
System.out.println("anyOf->" + a);
}
});

打印结果:

future1当前线程名:ForkJoinPool.commonPool-worker-1
future2当前线程名:ForkJoinPool.commonPool-worker-1
future3当前线程名:ForkJoinPool.commonPool-worker-2
future4当前线程名:ForkJoinPool.commonPool-worker-2
anyOf->0.02
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero

CompletableFuture的底层原理

CompletableFuture的核心构造:ForkJoinPool
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
  /**
   默认执行器—— ForkJoinPool.commonPool() 除非它不能支持并行性。
   */
  private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

  /**
   返回一个新的 CompletableFuture,它由在 ForkJoinPoolcommonPool() 中运行的任务异步完成,其值是通过调用给定的供应商获得的。
   * @param supplier a function returning the value to be used
   * to complete the returned CompletableFuture
   * @param  the function's return type
   * @return the new CompletableFuture
   */
  public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(asyncPool, supplier);
  }

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

CompleteableFuture到ForkJoinTask转换流程如下:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第9张图片

ForkJoinTask的类图如下:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第10张图片

CompleteableFuture类的接口层面与内部实现:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第11张图片

CompletableFuture任务的链式执行过程分析

以 supplyAsync().thenApply().thenRun()链式代码为例

第1步:CompletableFuture future1=CompletableFuture.supplyAsync()

ForkJoinPool执行一个ForkJoinTask类型的任务,即AsyncSupply。输入是Supply,输出结果存放在CompletableFuture中。

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第12张图片

第2步:CompletableFuture future2=future1.thenApply()

public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {
    return uniApplyStage(null, fn);
}
private CompletableFuture<Void> uniAcceptStage(Executor e, Consumer<? super T> f) {
    if (f == null) throw new NullPointerException();
    // 新创建了一个CompletableFuture
    CompletableFuture<Void> d = new CompletableFuture<Void>();
    // executor 传入的是null, d.uniaccept判断驱动thenAccept的CompletableFuture是否运行完/是否运行
    if (e != null || !d.uniAccept(this, f, null)) {
        // 用新创建的CompletableFuture和驱动thenAccept的CompletableFuture构建一个UniAccept
        UniAccept<T> c = new UniAccept<T>(e, d, this, f);
        // 放入栈中,此栈是第一步执行完,返回的CompletableFuture
        push(c);
        c.tryFire(SYNC);
    }
    return d;
}

第3步:CompletableFuture future3=future2.thenRun()

public CompletableFuture<Void> thenRun(Runnable action) {
    return uniRunStage(null, action);
}

private CompletableFuture<Void> uniRunStage(Executor e, Runnable f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<Void> d = new CompletableFuture<Void>();
    if (e != null || !d.uniRun(this, f, null)) {
        CompletableFuture.UniRun<T> c = new CompletableFuture.UniRun<T>(e, d, this, f);
        push(c);
        c.tryFire(SYNC);
    }
    return d;
}

第3步和第2步的过程类似,构建了一个 UniRun 对象,这个对象 被压入第2步CompletableFuture所在的栈中。当第2步的任务执行完成时,从自己的栈中弹出UniRun对象并执行。
请添加图片描述

thenApply与thenApplyAsync的区别
  • (1)如果前置任务没有完成,即a.result=null,则上面的uniApply会返回false,此时thenApply也会走到thenApplyAsync的逻辑里面,生成UniApply对象入栈;
  • (2)只有在前置任务已经完成的情况下,thenApply才会立即执行,不会入栈,再出栈,此时thenApply和thenApplyAsync才有区别。
  • 同理,thenRun与thenRunAsync、thenAccept与thenAcceptAsync的区别与此类似。
    核心源码如下:
public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {
    return uniApplyStage(null, fn);
}

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn) {
    return uniApplyStage(asyncPool, fn);
}

private <V> CompletableFuture<V> uniApplyStage(Executor e, Function<? super T, ? extends V> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<V> d = new CompletableFuture<V>();
    if (e != null || !d.uniApply(this, f, null)) {
        UniApply<T, V> c = new UniApply<T, V>(e, d, this, f);
        push(c);
        c.tryFire(SYNC);
    }
    return d;
}

//根据依赖的上一个阶段a是否完成,看要不要立即安排当前任务执行
//返回true表示已经同步完成执行了当前任务。为false表示依赖的阶段a还没完成,需要等待,或者已经安排异步执行(如果是异步任务的话)
final <S> boolean uniApply(CompletableFuture<S> a, Function<? super S, ? extends T> f, UniApply<S, T> c) {
    Object r;
    Throwable x;
    if (a == null || (r = a.result) == null || f == null) return false;//表示依赖的阶段a还没完成,还不能执行当前阶段
    tryComplete:
    if (result == null) { //依赖的阶段a已经完成,当前阶段还没完成
        if (r instanceof AltResult) {
            //如果依赖的阶段a是异常结束,那么当前阶段也异常结束
            if ((x = ((AltResult) r).ex) != null) {
                completeThrowable(x, r);
                break tryComplete;
            }
            r = null;
        }
        //到这里表示依赖的阶段a是正常结束
        try {
            if (c != null && !c.claim()) return false; //只有在c不为空,并且不能被执行或者已经安排异步执行才会返回false
            //拿到已经完成的依赖阶段a的结果,执行同步执行当前任务,并把结果设置到当前CompletableFuture阶段
            @SuppressWarnings("unchecked") S s = (S) r;
            completeValue(f.apply(s));
        } catch (Throwable ex) {
            //异常完成的处理
            completeThrowable(ex);
        }
    }
    return true;
}

//通过自定义TAG,标记任务正在被执行,保证任务只会被执行一次。
//该方法只会在不能被执行或者已经安排异步执行才会返回false
final boolean claim() {
    Executor e = executor;
    //解锁成功,表示可以执行了
    if (compareAndSetForkJoinTaskTag((short) 0, (short) 1)) {
        if (e == null) return true;    //需要被安排同步执行,立即返回true
        executor = null; // disable 赋值GC
        e.execute(this);    //否则立即安排异步执行
    }
    return false;
}
CompletableFuture任务的网状执行

有向无环图

如果任务只是链式执行,便不需要在每个CompletableFuture里面设1个栈了,用1个指针使所有任务组成链表即可。

但实际上,任务不只是链式执行,而是网状执行,组成1张图。
Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第13张图片

  • (1)在每个任务的返回值里面,存储了依赖它的接下来要执行的任务。
  • (2)任务2、任务3的CompletableFuture里面,都存储了任务5,那么任务5是不是会被触发两次,执行两次呢?任务5的确会被触发2次,但它会判断任务2、任务3的结果是不是都完成,如果只完成其中一个,它就不会执行。
  • (3)任务7存在于任务4、任务5、任务6的CompletableFuture的栈里面,因此会被触发三次。但它只会执行一次,只要其中1个任务执行完成,就可以执行任务7了。
  • (4)正因为有And和Or两种不同的关系,因此对应BiApply和OrApply两个对象,这两个对象的构造函数几乎一样,只是在内部执行的时候,一个是And的逻辑,一个是Or的逻辑。

正因为有And和Or 两种不同的关系,因此对应BiApply和OrApply两个对象,这两个对象的构造函数几乎一样,只是在内部执行的时候,一个是And的逻辑,一个是Or的逻辑。

static final class OrApply<T, U extends T, V> extends CompletableFuture.BiCompletion<T, U, V> {
  //        ...
  OrApply(Executor executor, CompletableFuture<V> dep, CompletableFuture<T> src, CompletableFuture<U> snd, Function<? super T, ? extends V> fn) {
    super(executor, dep, src, snd);
    this.fn = fn;
  }
  //        ...
}

static final class BiApply<T, U, V> extends CompletableFuture.BiCompletion<T, U, V> {
  //        ...
  BiApply(Executor executor, CompletableFuture<V> dep, CompletableFuture<T> src, CompletableFuture<U> snd, BiFunction<? super T, ? super U, ? extends V> fn) {
    super(executor, dep, src, snd);
    this.fn = fn;
  }
  //        ...
}

多元操作转换为二元操作

任何一个多元操作,都能被转换为多个二元操作的叠加。假如任务1And任务2And任务3=任务4,那么它可以被转换为右边的形式。新建了一个And任务,这个And任务和任务3再作为参数,构造任务4。
Or的关系,与此类似。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tGePldrW-1630666588875)(多元操作转换为二元操作.png)]

allOf内部的计算图分析

allOf核心源码

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
    return andTree(cfs, 0, cfs.length - 1);
}
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
    return orTree(cfs, 0, cfs.length - 1);
}
static CompletableFuture<Void> andTree(CompletableFuture<?>[] cfs, int lo, int hi) {
    CompletableFuture<Void> d = new CompletableFuture<Void>();
    if (lo > hi) // empty
        d.result = NIL;
    else {
        CompletableFuture<?> a, b;
        int mid = (lo + hi) >>> 1;
        if ((a = (lo == mid ? cfs[lo] : andTree(cfs, lo, mid))) == null ||
                (b = (lo == hi ? a : (hi == mid + 1) ? cfs[hi] : andTree(cfs, mid + 1, hi))) == null
            throw new NullPointerException();
        if (!d.biRelay(a, b)) {
            CompletableFuture.BiRelay<?, ?> c = new CompletableFuture.BiRelay<>(d, a, b);
            a.bipush(b, c);//把c压入a,b所在的栈中,因为c要等a,b都执行完成之后才能执行
            c.tryFire(SYNC);
        }
    }
    return d;
}
final void bipush(CompletableFuture<?> b, BiCompletion<?, ?, ?> c) {
    if (c != null) {
        Object r;
        while ((r = result) == null && !tryPushStack(c)) //c压入a的栈
            lazySetNext(c, null); // clear on failure
        if (b != null && b != this && b.result == null) {
            Completion q = (r != null) ? c : new CoCompletion(c);
            while (b.result == null && !b.tryPushStack(q)) //c压入b的栈
                lazySetNext(q, null); // clear on failure
        }
    }
}

allOf内部的运作过程

方块表示任务,椭圆表示任务的执行结果。假设allof的参数传入了future1、future2、future3、future4,则对应四个原始任务。生成BiRelay1、BiRelay2任务,分别压入future1/future2、future3/future4的栈中。无论future1或future2完成,都会触发BiRelay1;无论future3或future4完成,都会触发BiRelay2;生成BiRelay3任务,压入future5/future6的栈中,无论future5或future6完成,都会触发BiRelay3任务。

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第14张图片

BiRelay只是一个中转任务,它本身没有任务代码,只是参照输入的两个future是否完成。如果完成,就从自己的栈中弹出依赖它的BiRelay任务,然后执行。

优缺点

作为Java8的新特性,CompletableFuture广受好评,下面简单地做下总结。

优点
  • 1、充分利用多核CPU并行执行逻辑,效率高。
  • 2、API简单,还支持链式调用、异常处理等操作。
缺点
  • 1、JDK1.7及以前的项目不可用。
  • 2、不好控制CPU、内存等资源对使用情况,可能会导致系统宕机。

5.中间件+线程池

在不使用@Async、FutureTask、CompletableFuture的前提下实现异步执行任务。

核心思路

借助中间件存储任务队列信息(如Redis),使用定时任务从队列中拉取任务,使用线程池执行任务。

实现逻辑

  • 1、创建Redis任务队列、用户任务Map(非必须)。
  • 2、创建异步任务对象并设置属性。
  • 3、往任务队列里push任务,若要控制每个用户的任务提交量,则还需要给用户任务Map中put一个<用户id,用户任务队列>的Map,该过程耗时时间不长。
  • 4、启动定时任务执行任务队列的任务,成功与否都将任务从队列中移除,若出现异常,往钉钉群里发送邮件警告。

具体实现流程伪代码

注意:以下是伪代码,无法正常执行,只展示核心逻辑,源代码已经过验证没问题。

1、创建任务对象

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
class TaskPOJO {
  //用户id
  private Integer userId;
  //任务id
  private String taskId;
  //模拟任务
  private String task = "hello world!";
}

2、创建Redis任务队列、用户任务Map(非必须),编写提交异步任务的方法(建议写在Service)

public class XXXController {
    //单个用户最大任务数量
    private final static int MAX_TASK_NUM = 5;
    //定义Redis中任务队列名
    private final static String TASK_QUEUE_NAME = "";
    //定义Redis中用户任务队列名
    private final static String USER_TASK_MAP_NAME = "";
    // 任务队列
    private RQueue<TaskPOJO> taskQueue;
    // 用户任务队列
    private RMapCache<Long, Set<String>> userCachedCommitTaskMap;
    @Resource
    private RedissonClient redissonClient;
    @PostConstruct
    public void init() {
        taskQueue = redissonClient.getQueue(TASK_QUEUE_NAME);
        userCachedCommitTaskMap = redissonClient.getQueue(USER_TASK_MAP_NAME);
    }

    /**
     * 异步提交任务
     */
    public void asyncTask() {
        Integer userId = 1;
        String taskId = "001";
        TaskPOJO task = new TaskPOJO(userId, taskId, "测试001");
        Set<String> taskIds = userCachedCommitTaskMap.get(userId);
        if (CollectionUtils.isNotEmpty(taskIds) && taskIds.size() > MAX_TASK_NUM) {
            //报个自定义异常,不允许提交
        }
        taskQueue.add(task);
        taskIds.add(taskId);
        userCachedCommitTaskMap.set(userId, taskIds);
    }
}

3、使用定时任务+线程池自动从Redis队列中拉取任务执行

@Slf4j
@Component
public class TaskExecuteJob {
  //分布式锁名
  private final static String R_LOCK_NAME = "";
  @Resource
  private RedissonClient redissonClient;
  //使用线程池,此处用《阿里巴巴java开发手册》不建议的写法,实际工作中可以根据实际情况配置线程池参数
  private final ExecutorService executorService = Executors.newFixedThreadPool(60);

  /**
   * 每N秒执行一次
   */
  @Scheduled(cron = "0/5 * * * * ?")
  public void bathSendMailScheduled() {
    RLock lock = redissonClient.getLock(R_LOCK_NAME);
    if (lock.isLocked()) {
      return;
    }
    RQueue<TaskPOJO> redisQueue = redissonClient.getQueue(TASK_QUEUE_NAME);
    if (CollectionUtils.isEmpty(redisQueue)) {
      return;
    }
    TaskPOJO taskPOJO;
    try {
      lock.lock(15, TimeUnit.MINUTES);//分布式锁自动释放时间不能随便设置,在此不考虑存在的问题
      taskPOJO = redisQueue.poll();
      String task = taskPOJO.getTask();
      //执行具体逻辑
      executorService.execute(() -> System.out.println(task));
    } catch (Exception e) {
      //发送异常通知
    } finally {
      //从队列中移除任务id
      redisQueue.remove(taskPOJO.getTaskId());
      //同时删除用户任务队列中的taskId,此处省略代码
      lock.unlock();
    }
  }
}

优缺点

用这种方式执行异步任务缺点很明显,但是也有它的优势,以下简单地对它的优缺点做下总结。

  • 优点
    • 1、使用Redis队列存储异步任务,若异步任务需要被撤回、修改属性,直接从队列中修改未执行的任务即可。
    • 2、借助线程池执行,线程池参数可控制。可根据任务量动态对硬件、配置参数进行动态扩容、缩容,避免大量的异步任务在同一个服务或同一台机器上执行导致宕机。
  • 缺点
    • 1、使用定时任务拉取任务,会存在停顿时间,降低执行效率。
    • 2、高可用问题,Redis挂掉、机器挂掉会引起一系列问题。
    • 3、对中间件依赖比较高。

6.加餐:Java响应式编程Reactive

响应式编程是一种编程概念,当前Java中没有标准统一的响应式API实现。当前有大量的库提供不同的实现,以及工具运行响应式编程。

同步非阻塞

响应式编程是观察者模式的扩展,以RxJava中的实现为例:

import java.util.Observable;

public class Main {
  public static void main(String[] args) {
    //可观察对象
    MyObservable observable = new MyObservable();
    //添加观察者
    for (int i = 0; i < 10; i++) {
      int finalI = i;
      observable.addObserver((o, arg) -> System.out.println("观察者" + finalI + "处理事件:" + arg.toString()));
    }
    //发布事件通知观察者
    observable.setChanged();
    //如果该对象发生了变化,由所示hasChanged方法,则通知其所有观察者,并调用clearChanged方法来指示该对象不再改变。
    observable.notifyObservers("事件" + Thread.currentThread().getName());
  }

  static class MyObservable extends Observable {
    @Override
    public void setChanged() {
      //将此Observable对象标记为已更改; hasChanged方法现在将返回true 。
      super.setChanged();
    }
  }
}

执行结果:

观察者9处理事件:事件main
观察者8处理事件:事件main
观察者7处理事件:事件main
观察者6处理事件:事件main
观察者5处理事件:事件main
观察者4处理事件:事件main
观察者3处理事件:事件main
观察者2处理事件:事件main
观察者1处理事件:事件main
观察者0处理事件:事件main

可以看出,代码的执行顺序并非按照我们的代码顺序执行,而是反过来,都是在main线程中同步执行。这种方式可以称为同步非阻塞的响应式编程。

异步非阻塞

既然有同步式的非阻塞,那就有异步非阻塞的响应式编程,在Java中的Swing就是一个很好的例子。

import javax.swing.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class Main {
    public static void main(String[] args) {
        JFrame jFrame = new JFrame();
        jFrame.setVisible(true);
        jFrame.setBounds(200, 200, 400, 400);

        jFrame.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                //在组件上单击(按下和释放)鼠标按钮时调用。
                super.mouseClicked(e);
                System.out.println("鼠标点击事件" + Thread.currentThread().getName());
            }
        });

        jFrame.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                //当组件获得键盘焦点时调用
                super.focusGained(e);
                System.out.println("焦点事件" + Thread.currentThread().getName());
            }
        });
    }
}

执行结果:

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第15张图片

焦点事件AWT-EventQueue-0
焦点事件AWT-EventQueue-0
鼠标点击事件AWT-EventQueue-0
焦点事件AWT-EventQueue-0
鼠标点击事件AWT-EventQueue-0

在swing的事件响应处理并不是在main线程里面进行处理的,鼠标点击和焦点事件处理都是在一个叫AWT-EventQueue-0的线程中进行处理,可以看见这种异步处理的方式有别于上面的观察者模式。

这里对于事件的响应更加类似于一种对事件进行拉取的方式,点击窗体,发现打印鼠标事件是有延迟的,原因就是这里对于事件的获取是采用另起一个线程轮询策略,监听到对应的事件之后委托给对应的事件处理器(回调函数)进行处理,这种方式叫异步非阻塞。

想一想,NIO、AIO是响应式编程吗?

Java异步编程源码及原理解析:Future、FutureTask、@Async、ForkJoin框架、CompletableFuture、Redis队列、Reactive响应式编程的优缺点及适用场景_第16张图片

此外,还有消息队列等其他方式也可以实现异步任务。

未完待续。。。

你可能感兴趣的:(源码,Java,#,Java基础,java,异步,响应式编程,future,多线程)