Java多任务编排技术

JDK 5新增Future接口,用于处理异步计算结果。虽然Future提供异步执行任务能力,但是获取结果很不方便,要么通过Future#get阻塞调用线程,或者通过轮询 Future#isDone判断任务是否结束,再获取结果。

因此,Java 8新特征引入异步线程编排工具CompletableFuture,用于编排与构建异步处理任务。CompletableFuture实现CompletionStage和Future接口,增加异步会点、流式处理、多Future组合处理能力,目的是简化编排多任务协同工作,提高任务执行速率。

01 Future

Future两种调用方式都不是很优雅,比如通过Future#get阻塞调用线程,或者通过轮询 Future#isDone判断任务是否结束再获取结果,都存在无效等待阻塞或者判断,吞吐量不高。

1.1 使用案例

java 复制代码
public class Main {
    public static void main(String[] args)  throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        Future<String> future = executorService.submit(() -> {
            Thread.sleep(2000);
            return "hello";
        });
        System.out.println(future.get());
        System.out.println("end");
    }
}

1.2 多任务解决方案

与此同时,Future无法解决多个异步任务相互依赖场景。简单点说,就是主线程需要等待子线程任务执行完毕后再进行执行。不过,是否会想到通过CountDownLatch解决,没错确实可以。案例模拟拆分多任务并发计算,最好进行数据汇总统计设计过程。

java 复制代码
public class Main {
    private static int num = 100;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

       int step = 5;
       int taskCount = num / step;

        long startTime = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(taskCount);

        List<Future<Long>> futures = new ArrayList<>();
        for (int i = 0; i < taskCount; i++) {
            int start = i * step;
            int end = start + step;

            Future<Long> future = executorService.submit(() -> {
                long result = 0L;
                for (int j = start; j < end; j++) {
                    result += (long) j * j;
                }
                TimeUnit.MILLISECONDS.sleep(50);
                latch.countDown();
                return result;
            });
            futures.add(future);
        }

        // 等待计算处理完成
        latch.await();

        // 计算结果
        long result = 0;
        for (Future<Long> future : futures) {
            result += future.get();
        }

        long costMills = System.currentTimeMillis() - startTime;
        System.out.printf("result=%d, costTime=%dms", result, costMills);
    }
}
java 复制代码
public class Main {
    private static int num = 100;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

       int step = 5;
       int taskCount = num / step;

        long startTime = System.currentTimeMillis();

        List<Future<Long>> futures = new ArrayList<>();
        for (int i = 0; i < taskCount; i++) {
            int start = i * step;
            int end = start + step;

            Future<Long> future = executorService.submit(() -> {
                long result = 0L;
                for (int j = start; j < end; j++) {
                    result += (long) j * j;
                }
                TimeUnit.MILLISECONDS.sleep(50);
                return result;
            });
            futures.add(future);
        }

        // 等待任务完成
        Set<Future<Long>> completeFutures = new HashSet<>();
        while(completeFutures.size() < futures.size()) {
            Iterator<Future<Long>> iter = futures.iterator();
            while(iter.hasNext()) {
                Future<Long> future = iter.next();
                if(future.isDone() || future.isCancelled()) {
                    completeFutures.add(future);
                    iter.remove();
                }
            }
        }

        // 计算结果
        long result = 0;
        for (Future<Long> future : completeFutures) {
            result += future.get();
        }

        long costMills = System.currentTimeMillis() - startTime;
        System.out.printf("result=%d, costTime=%dms", result, costMills);
    }
}

通过代码看出,额外通过CountDownLatch变量统计多任务处理进度,增加代码复杂度和处理时间耗时,编码不优雅,要彻底解决就需要用到Java 8推荐的CompletableFuture异步编排工具类。

02 CompletableFuture

2.1 抛砖引玉

CompletableFuture无须通过任何同步控制工具,就可以轻松解决Future多任务协同处理问题。别以为这就结束了,远不止于此,CompletableFuture比这要厉害很多。

java 复制代码
public class Main {
    private static int num = 100;

    public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();

        int step = 5;
        int taskCount = num / step;
        CompletableFuture[] futures = new CompletableFuture[taskCount];
        for (int i = 0; i < taskCount; i++) {
            int start = i * step;
            int end = start + step;
            CompletableFuture<Long> future = CompletableFuture.supplyAsync(() -> {
                long result = 0L;
                for (int j = start; j < end; j++) {
                    result += (long) j * j;
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(550);
                } catch(Exception e) {
                    
                }
                return result;
            });
            futures[i] = future;
        }

        // 等待计算完成完成
        CompletableFuture.allOf(futures).get();

        // 计算结果
        long result = 0;
        for (Future<Long> future : futures) {
            result += future.get();
        }

        long costMills = System.currentTimeMillis() - startTime;
        System.out.printf("result=%d, costTime=%dms", result, costMills);
    }
}

2.2 常用创建方式

CompletableFuture提供4个静态方法执行异步任务,主要区别有是否支持返回值,以及是否支持指定线程池。如果不支持指定线程池,默认使用<font style="color:#74B602;">ForkJoinPool.commonPool()</font>提供的默认线程池。

方法 区别
supplyAsync 执行任务,支持返回值
runAsync 执行任务,不支持返回值
java 复制代码
// 使用默认内置线程池, 根据Supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 提供指定自定义线程池, 根据Supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

// 使用默认内置线程池, 根据Runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable) 
// 提供指定自定义线程池, 根据Runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor)

2.3 常用获取结果方式

方法 区别
get 如果需要指定时间获取,会抛出超时异常,以及执行过程异常
getNow 不阻塞立即获取结果,返回已完成结果或异常,否则返回设定默认值
join 阻塞获取结果,会抛出执行异常
java 复制代码
// 阻塞获取值
public T get()
// 设定超时时间获取值
public T get(long timeout, TimeUnit unit)
// 立刻获取值
public T getNow(T valueIfAbsent)
// 等待获取值
public T join()

2.4 异步回调

方法 区别
thenRun/thenRunAsync 不依赖上个任务返回结果,无入参,无返回值
thenAccept/thenAcceptAsync 依赖上个任务返回结果,有入参,无返回值
thenApply/thenApplyAsync 依赖上个任务返回结果,有入参,有返回值
exceptionally 任务异常回调方法
whenComplete 任务执行完成回调方法,无返回值
handle 任务执行完成回调方法,有返回值
java 复制代码
public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)

public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

public CompletableFuture<Void> thenApply(Function<? super T,? extends U> fn)
public CompletableFuture<Void> thenApplyAsync(Function<? super T,? extends U> fn)
public CompletableFuture<Void> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

public CompletionStage<T> exceptionally(Function<Throwable, ? extends T> fn)

public CompletionStage<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
public CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
public CompletionStage<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)

thenRun:方法运行任务与上一个任务公用一个线程池

java 复制代码
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    static boolean useCommonPool = (ForkJoinPool.getCommonPoolParallelism() > 1);
    static Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
    
    public CompletableFuture<Void> thenRun(Runnable action) {
        return uniRunStage(null, action);
    }
    public CompletableFuture<Void> thenRunAsync(Runnable action) {
        return uniRunStage(asyncPool, action);
    }
}

thenRunAsync:方法运行任务使用ForkJoinPool公共线程池,所以不建议程序使用,因为无法控制程序其他地方是否存在调用情况。

补充说明,类似thenAccept和thenAcceptAsync、thenApply和thenApplyAsync之间也是同样差别。

2.5 完成回调

CompletableFuture任务不论正常与否,都会回调whenComplete方法。也就是,调用 <font style="color:rgb(44, 62, 80);">get()</font>方法时正常完成就获取到结果,处理异常就会抛出异常,whenComplete需要处理异常。

方法 区别
正常完成 whenComplete返回结果和上级任务一致,异常为null;
执行异常 whenComplete返回结果为null,异常为上级任务异常
java 复制代码
 public class Main {
    public static void main(String[] args) throws Exception {
        CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                throw new RuntimeException("错误路径");
            }
            System.out.println("处理正常");
            return 0.11;
        }).whenComplete((res, throwable) -> {
            if (res == null) {
                System.out.println("whenComplete res is null");
            } else {
                System.out.println("whenComplete res is " + res);
            }
            if (throwable == null) {
                System.out.println("whenComplete throwable is null");
            } else {
                System.out.println("whenComplete throwable is " + throwable.getMessage());
            }
        }).exceptionally((throwable) -> {
            System.out.println("exceptionally throwable is " + throwable.getMessage());
            return 0.0;
        });
        System.out.println("result = " + future.get());
    }
}

2.6 多任务组合回调

2.6.1 AND组合

CompletableFuture提供thenCombine、thenAcceptBoth和runAfterBoth,用于前面所有任务都执行完成,再进行后续任务处理。

方法 区别
runAfterBoth 后续任务无入参,无返回值
thenAcceptBoth 接收前面任务结果参数,无返回值
thenCombine 接收前面任务结果参数,有返回值
java 复制代码
 public class Main {
    public static void main(String[] args) throws Exception {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 异步任务1
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("[task 1] thread is " + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("[task 1] end");
            return result;
        }, executorService);
    
        // 异步任务2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("[task 2] thread is " + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("[task 2] end");
            return result;
        }, executorService);
    
        // 任务组合
        CompletableFuture<Integer> task3 = task1.thenCombineAsync(task2, (f1, f2) -> {
            System.out.println("[task 3] thread is " + Thread.currentThread().getId());
            System.out.println("[task 1] res = " + f1);
            System.out.println("[task 2] res = " + f2);
            return f1 + f2;
        }, executorService);
    
        Integer res = task3.get();
        System.out.println("res = " + res);
    }
}

2.6.2 OR组合

CompletableFuture提供applyToEither、acceptEither和runAfterEither,用于前面任务只需要存在执行完成,就可以进行后续任务处理。

方法 区别
runAfterEither 后续任务无入参,无返回值
acceptEither 接收前面任务结果参数,无返回值
applyToEither 接收前面任务结果参数,有返回值
java 复制代码
 public class Main {
    public static void main(String[] args) throws Exception {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 异步任务1
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("[task 1] thread is " + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("[task 1] end");
            return result;
        }, executorService);
    
        // 异步任务2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("[task 2] thread is " + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("[task 2] end");
            return result;
        }, executorService);
    
        // 任务组合
        CompletableFuture<Integer> task3 = task1.acceptEitherAsync(task2, (f) -> {
            System.out.println("[task 3] thread is " + Thread.currentThread().getId());
            System.out.println("[task] res = " + f);
        }, executorService);
    
        Integer res = task3.get();
        System.out.println("res = " + res);
    }
}

2.6.3 并行组合

CompletableFuture提供allOf或者anyOf用于多任务进行编排处理,轻松处理多任务协同工作。

方法 区别
allOf 等待所有任务完成
anyOf 任何一个任务完成
java 复制代码
 public class Main {
    public static void main(String[] args) throws Exception {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 异步任务1
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("[task 1] thread is " + Thread.currentThread().getId());
            int result = 1 + 1;
            System.out.println("[task 1] end");
            return result;
        }, executorService);
    
        // 异步任务2
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("[task 2] thread is " + Thread.currentThread().getId());
            int result = 1 + 2;
            System.out.println("[task 2] end");
            return result;
        }, executorService);
        
        // 异步任务3
        CompletableFuture<Integer> task3 = CompletableFuture.supplyAsync(() -> {
            System.out.println("[task 3] thread is " + Thread.currentThread().getId());
            int result = 1 + 3;
            System.out.println("[task 3] end");
            return result;
        }, executorService);

        // 任务组合
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(task1, task2, task3);
        // 只要有一个有任务完成
        Object o = anyOf.get();
        System.out.println("[any task] res =" + o);
    
        // 任务组合
        CompletableFuture<Void> allOf = CompletableFuture.allOf(task1, task2, task3);
    
        // 等待所有任务完成
        allOf.get();
        
        // 获取任务的返回结果
        System.out.println("[task 1] res = " + task1.get());
        System.out.println("[task 2] res = " + task2.get());
        System.out.println("[task 3] res = " + task3.get());
    }
}

03 CompletableFuture使用建议

3.1 必须提取结果才能捕获异常

CompletableFuture需要获取返回值,才能获取到异常信息。如果不调用get()/join()方法,无法判断处理任务是否异常。不过,也可以考虑通过try...catch...代码块或者exceptionally方法处理。

java 复制代码
@Test
public void testWhenCompleteExceptionally() {
    CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {
        if (1 == 1) {
            throw new RuntimeException("出错了");
        }
        return 0.11;
    });

    // 如果不调用get()方法,无法判断处理是否异常
    // future.get();
}

3.2 CompletableFuture#get是阻塞方法

CompletableFuture>#get()是阻塞方法,如果需要异步调用获取返回值,需要添加超时时间。

java 复制代码
// 反例
CompletableFuture.get();
// 正例
CompletableFuture.get(5, TimeUnit.SECONDS);

3.3 谨慎使用默认线程池

CompletableFuture使用默认ForkJoin线程池, 处理线程数=电脑CPU核数-1 。如果存在大量请求,处理逻辑复杂响应会很慢。一般建议使用自定义线程池,优化线程池配置参数。

3.4 自定义线程池注意饱和策略

CompletableFuture使用自定义线程池,如果线程池拒绝策略为DiscardPolicy或者DiscardOldestPolicy,线程池饱和会直接丢弃任务,不会抛弃异常。因此,建议CompletableFuture线程池策略根据场景选择合适拒绝策略,耗时的异步线程建议做好线程池隔离。

相关推荐
重庆小透明12 分钟前
力扣刷题记录【1】146.LRU缓存
java·后端·学习·算法·leetcode·缓存
lang2015092817 分钟前
Reactor操作符的共享与复用
java
TTc_28 分钟前
@Transactional事务注解的批量回滚机制
java·事务
wei_shuo1 小时前
飞算 JavaAI 开发助手:深度学习驱动下的 Java 全链路智能开发新范式
java·开发语言·飞算javaai
欧阳秦穆2 小时前
apoc-5.24.0-extended.jar 和 apoc-4.4.0.36-all.jar 啥区别
java·jar
岁忧2 小时前
(LeetCode 面试经典 150 题 ) 58. 最后一个单词的长度 (字符串)
java·c++·算法·leetcode·面试·go
Java初学者小白2 小时前
秋招Day14 - Redis - 应用
java·数据库·redis·缓存
代码老y2 小时前
Spring Boot + 本地部署大模型实现:优化与性能提升
java·spring boot·后端
GodKeyNet2 小时前
设计模式-桥接模式
java·设计模式·桥接模式