Java 异步编程

391 阅读8分钟

在java编程过程中,针对并发和多线程的环境的处理能体现程序员的基本素养。从最基本的 synchronized 到ReentrantLock 和 ReentrantReadWriteLock、再到 StampedLock,最后在分布式场景下的分布式锁,从Thread 到线程池,另外还有 CountDownLatch、CyclicBarrier 和 Semaphore。

1 线程池应用

在实际的应用场景中,线程池的应用是相当的广泛,除了 jdk 自带的四种固定的线程池外,还有不少的开源框架提供线程池的创建方法,例如 guava,apache 、hutools。不过一般情况下大家都是根据实际的业务场景使用 ThreadPoolExecutor 进行线程池参数的配置。此外在开发中,为了避免线程池的频繁创建,也会将线程池声明为一个bean,交给spring 进行管理,既方便对线程池的管理,也方便项目中业务场景的调用。

一般情况下线程池的创建方式,使用 ThreadPoolExecutor 的创建方式:

// 线程工厂
ThreadFactory factory = (runnable) ->{
    return new Thread(runnable, "demo-server-" + runnable.hashCode());
};
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,4,
        3000,TimeUnit.SECONDS,
        new LinkedBlockingDeque<>(300),  factory,
        RejectPolicy.CALLER_RUNS.getValue());

当创建需要执行批量任务时,只需要将任务放进线程池即可,如果需要进行并发控制,则可以将 CountDownLatch、Semaphore 传入到线程中进行访问控制, 任务执行完毕后关闭线程池即可。针对 cpu密集型和 io 密集型的任务,可以使用经验来控制线程池的核心线程数和最大线程数使得系统资源得到充分地利用。

如上图所示,假设一共有四个任务执行,花费的时间如 time line 所示,如果需要获取每个任务执行的结果,那么我们写的 Task 类就需要继承 Callable 而不是 Runnable, 提交任务的方法也需要从 execute 改为 submit,从而获取每个任务返回的 Future 对象。那么这就有一个问题了,如果我们将执行的结果放进集合中进行存储,然后遍历获取结果时,future.get()的方法是阻塞的,这样我们就只能按照放置任务的顺序去获取执行的结果。其实我们应该按照任务执行完成的顺序去获取结果,而不是按照执行的顺序。这时候就可以请出来 CompletionService 了。

class Task implements Callable {
  // 省略代码
}
// 存放结果,将 future 放入 resultList 中
List<Future> resultList = new ArrayList<>();
Future fut = executor.submit(new Task());
resultList.add(fut);
// 遍历 resultList 获取执行的结果

改进后的使用方法,就是使用 CompletionService 对 executor 进行了一层包装:

CompletionService service = new ExecutorCompletionService(executor);
List<Future> resultList = new ArrayList<>();
// 如果不使用 list 接收结果的话,可以 for 循环任务数量,使用 service.take() 的方式阻塞获取执行结果
Future fut = service.submit(new Task());
resultList.add(fut);

CompletionService 之所以可以按照任务的完成顺序进行返回结果,是因为其在内部维护了一个 BlockingQueue<Future<V>> completionQueue 存储执行完成的任务。使用这种形式之后,我们就可以按照任务完成的顺序来获取结果,进一步地缩短了时间,提高了程序运行的效率。

2 异步编程

线程池能结果项目中的大部分问题,但是线程池也不是万能的,有些问题实现起来就比较复杂,比如下图的需求:

003.jpg

在这种情况下使用线程池或者Thread 通过线程间通讯的方式实现已经过于复杂,因此就需要引入 CompletableFuture 来解决这个复杂的问题。先来看一下对应的代码,然后再进行分析和解释:

1 不同的颜色可以代表为不同的任务,不同任务之间是可以并行执行的。

2 任务需要按照流程顺序进行执行,不能出现乱序的情况。

2.1 代码示例

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
     log.info("起床时刻");
     log.info("准备早餐");
     log.info("洗漱中...");
     sleep(RandomUtil.randomInt(5, 10));
     return "洗漱完成";
 });

 CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
     sleep(RandomUtil.randomInt(1, 2));
     log.info("加热牛奶中...");
     sleep(RandomUtil.randomInt(2, 5));
     log.info("加热面包中...");
     sleep(RandomUtil.randomInt(2, 5));
     return "早饭完成";
 });

 CompletableFuture<String> result = f1.thenCombine(f2, (res1, res2) -> {
     String detail = StrUtil.format("{} -> {} ,可以准备吃饭了", res1, res2);
     log.info(detail);
     sleep(RandomUtil.randomInt(1, 2));
     return "出门上班";
 });
 // 获取结果
 log.info(result.join());

 CompletableFuture<String> f3 =
         CompletableFuture.supplyAsync(() -> {
             int t = RandomUtil.randomInt(1, 3);
             sleep(t);
             return "53路公交车";
         });

 CompletableFuture<String> f4 =
         CompletableFuture.supplyAsync(() -> {
             int t = RandomUtil.randomInt(0, 4);
             sleep(t);
             return "89路公交车";
         });

CompletableFuture<String> f6 =
         CompletableFuture.supplyAsync(() -> {
             int t = RandomUtil.randomInt(1, 2);
             sleep(t);
             return "80路公交车";
         });

 CompletableFuture<String> re3 = CompletableFuture.anyOf(f3, f4, f6).thenApply(res -> {
     log.info("座上了 {}", res);
     sleep(RandomUtil.randomInt(3));
     int i = RandomUtil.randomInt(10);
     if (i < 2) {
         throw new RuntimeException("突然身体不舒服,去医院看病了");
     } else if (i < 4) {
         throw new RuntimeException("突然不想上班了,直接旅游去了");
     } else if (i < 6) {
         throw new RuntimeException("公交车坐反了,干脆请假了");
     }

     return "抵达公司,开始上班";
 }).exceptionally(ex -> {
     return  "ERROR:: " + ex.getMessage();
 }).handle((res, ex) -> {
        if (ex == null) {
             log.info("handle {}", res);
        } else {
             log.error("error {}", ex.getMessage());
        }
        return "结束";
 });

 try {
     log.info("最终结果{}", re3.get());
 } catch (InterruptedException e) {
     e.printStackTrace();
 } catch (ExecutionException e) {
     e.printStackTrace();
 }

最终程序运行的结果如下所示,可以看到基本满足了需求:

005.jpg

在异步编程中,所有的关系都可以描述为串行、并行和汇聚关系,汇聚就是表示将串行或者并行的结果进行合并处理: 在介绍异步编程的 API 之前,先介绍一下 java8 的函数式编程接口:

// Consumer 接口,接收一个参数,不返回结果
Consumer<String> consumer = (node) -> { System.out.println(node); };
consumer.accept("消费一个参数");

// Supplier 接口,无需入参,返回一个结果
Supplier<String> supplier = () -> {return "34";};
System.out.println(supplier.get());

// Function 接口,接收参数并返回一个结果,转换字符串为数字
 Function<String,Integer> function = (node) ->{return Integer.valueOf(node)};
function.apply("34");
// 此外还有 BiFunction BiConsumer,只不过是接收两个参数而已
BiConsumer<String,String> consumer = (node1, node2) -> {
    System.out.println(node1 + " : " + node2);
};
consumer.accept("44","55");
// 计算两个数字之和
BiFunction<String,String,Integer> function = (node1,node2) ->{return Integer.valueOf(node1) + Integer.valueOf(node2);};
function.apply("34","45");

2.2 串行关系

在异步编程中,所有的关系都可以描述为串行、并行和汇聚关系,汇聚就是表示将串行或者并行的结果进行合并处理: 在 CompletableFuture 中表述 串行关系的有 thenApply、thenAccept、thenRun 和 thenCompose:

thenApply 需要传入一个 Function, 将上一步的结果作为入参传入并进行计算返回

// 异步生产一个字符串,并将结果添加后缀后返回,生产字符串是一个异步任务,添加后缀使用的是生产字符串的线程,如果添加后缀的操作也需要另起线程,则需要使用 thenApplyAsync。
CompletableFuture<String> temp = CompletableFuture.supplyAsync(() -> {
       return "生产一个字符串";
});
// 
CompletableFuture<String> result = temp.thenApply(node -> {
    return node + ":添加后缀";
});
// 最后输出结果 [生产一个字符串:添加后缀]
System.out.println(result.get()); 
// 查阅一下源码,可以看到如下内容,xxx 和 xxxAsync 的区别就在于是否使用新的线程池
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);
}

thenAccept 需要传入一个 Consumer,将上一步的结果作为入参,但是无需返回结果

// 相同的,异步输出结果可以采用 thenAcceptAsync
CompletableFuture<Void> future = temp.thenAccept(node -> {
     System.out.println("已经生成了 -> " + node); 
});

thenCompose 也需要传入一个Function,将上一步的结果作为入参传入并进行计算返回

// 对比一下 thenApply 可以得知, thenApply 没有要求返回值的类型,而thenCompose 的返回结果必须是 CompletionStage 类型,而 CompletionStage 是 CompletableFuture 的父类,因此我们返回一个 CompletableFuture 对象即可,除此之外和 thenApply 无区别。
public <U> CompletableFuture<U> thenCompose(
     Function<? super T, ? extends CompletionStage<U>> fn) {
     return uniComposeStage(null, fn);
}
// 异步返回一个 CompletableFuture 对象
CompletableFuture<String> compose = temp.thenCompose(node ->{
    return CompletableFuture.supplyAsync(() ->{
        return node + "结果";
    });
});

本例中使用了 thenApply,是在选择哪个公交车先到后乘坐去上班,这里有三趟公交都可以抵达,因此使用了 anyOf(), 同样的,如果需要所有的条件都满足后在进行,可以使用 allOf进行处理。thenApply 去执行时是采用 CompletableFuture 中空闲的线程进行执行,如果需要另外的线程池去执行,则需要使用 thenApplyAsync, 表示异步执行。

// 只要有一个返回即可
CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
// 必须所有的都要返回
CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

thenRun 需要传入一个 Runnble 对象进行异步处理,这里就略过不举例说明了。

2.3 并行关系

表述并行关系的有 thenCombine、thenAcceptBoth、runAfterBoth:

thenCombine 需要传入两个参数,一个CompletionStage 对象,一个 BiFunction 对象,然后将两个任务的结果交给 BiFunction 进行处理。

// 案例中使用的 thenCombine 就是将两个异步任务的结果进行合并处理 
CompletableFuture<String> result = f1.thenCombine(f2, (res1, res2) -> {
     String detail = StrUtil.format("{} -> {} ,可以准备吃饭了", res1, res2);
     log.info(detail);
     sleep(RandomUtil.randomInt(1, 2));
     return "出门上班";
 });

有了串行关系的表述,那么 thenAcceptBoth、runAfterBoth 应该就可以猜出来了:

thenAcceptBoth 需要传入两个参数,一个CompletionStage 对象,一个 BiConsumer 对象,然后将两个任务的结果交给 BiConsumer 进行处理,没有返回结果。

runAfterBoth需要传入两个参数,一个CompletionStage 对象,一个 Runnable 对象,不接受两者的结果,直接进行异步任务处理。

2.4 聚合关系:

聚合关系也包括 and 和 or 两种关系,and 需要两者都执行完成后才能进行处理,or 则需要其中一个满足条件即可实现。在表述并行关系中已经 提到过 thenCombine,这里对 applyToEither 进行一下说明和介绍:

# 聚合 and 的 api
thenCombine、 thenAcceptBoth、runAfterBoth
# 聚合 or 的 api
applyToEither、acceptEither、 runAfterEither 

applyToEither 需要传入两个参数,一个CompletionStage 对象,一个 Function 对象,然后任何一个先执行完成的结果交给 Function 进行处理,并将结果返回

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(()->{
    return "f1";
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(()->{
    return "f2";
});
CompletableFuture<String> either = f1.applyToEither(f2, (node) -> {
    return node;
});

acceptEither 需要传入两个参数,一个CompletionStage 对象,一个 Consumer 对象,然后任何一个先执行完成的结果交给 Consumer 进行处理,没有返回结果

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(()->{
    return "f1";
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(()->{
    return "f2";
});
CompletableFuture<String> either = f1.applyToEither(f2, (node) -> {
    System.out.println("最终结果为 ::" + node);
});

2.5 异常处理:

异常处理可以在执行异步操作之后的任何一个步骤进行添加,拦截对应的异常并进行包装处理,最后使用 handle 或者 whenComplete 进行处理。

// exceptionally、 whenComplete、 handle
// 区别在于一个是 BiFunction ,一个是 BiConsumer,是否将结果返回
CompletableFuture<U> handle(
        BiFunction<? super T, Throwable, ? extends U> fn)
        
CompletableFuture<T> whenComplete(
        BiConsumer<? super T, ? super Throwable> action)        

2.6 异步处理:

最后讲一下 CompletableFuture.supplyAsync,这个应该是在最开始讲的,本来以为不重要,最后还是要简单提一下, 这个和函数式编程类似,是否需要有返回值的区别。

# runAsync 异步处理任务,但是没有返回值
runAsync 
# supplyAsync 异步处理任务,会有返回值
supplyAsync