java常用异步调用

JAVA8之前的异步编程

  • 继承Thead类,重写run方法
  • 实现runable接口,实现run方法
  • 匿名内部类编写thread或者实现runable的类,当然在java8中可以用lambda表达式简化
  • 使用futureTask进行附带返回值的异步编程
  • 使用线程池和Future来实现异步编程
  • spring框架下的@async获得异步编程支持

下面本文尝试将Java异步调用的多种方式进行归纳。

一、通过创建新线程

 首先的我们得认识到,**异步调用的本质,其实是通过开启一个新的线程来执行。**如以下例子:

public static void main(String[] args) throws Exception{

    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());

    new Thread(() -> {
        System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
    }).start();

    Thread.sleep(2000);

    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());

}

以上是采用Runable实现多线程创建方式的lambda写法,常见的多线程的实现方式有:1)继承Thread类,重写run()方法。2)实现runnable接口,重写run()方法。3)实现callable和future接口。

主线程 =====> 开始 =====> 1627893837146
异步线程 =====> 开始 =====> 1627893837200
主线程 =====> 结束 =====> 1627893839205
异步线程 =====> 结束 =====> 1627893842212

二、通过线程池

2.1 普通线程池

因为异步任务的实现本质的由新线程来执行任务,所以通过线程池的也可以实现异步执行。写法同我们利用线程池开启多线程一样。但由于我们的目的不是执行多线程,而是异步执行任务,所以一般需要另外一个线程就够了。

因此区别于执行多线程任务的我们常用的newFixedThreadPool,在执行异步任务时,我们用newSingleThreadExecutor 来创建一个单个线程的线程池

public static void main(String[] args) throws Exception{
    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.submit(()->{
        System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
    });
    executorService.shutdown(); // 回收线程池
    Thread.sleep(2000);
    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());
}

可以看到,结果跟第一种结果是基本一致的。温馨提示:不要忘记线程池的回收

主线程 =====> 开始 =====> 1627895467578
异步线程 =====> 开始 =====> 1627895467635
主线程 =====> 结束 =====> 1627895469644
异步线程 =====> 结束 =====> 1627895472649

2.2 线程池与future实现异步编程

       //构造线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        try {
            //构造future结果,doSomethingA十分耗时,因此采用异步
            Future<Integer> future = pool.submit(() -> doSomethingA());
            //做一些别的事情
            doSomethingB();
            //从future中获得结果,设置超时时间,超过了就抛异常
            Integer result = future.get(10, TimeUnit.SECONDS);
            //打印结果
            System.out.printf("the async result is : %d", result);
            //异常捕获
        } catch (InterruptedException e) {
            System.out.println("任务计算抛出了一个异常!");
        } catch (ExecutionException e) {
            System.out.println("线程在等待的过程中被中断了!");
        } catch (TimeoutException e) {
            System.out.println("future对象等待时间超时了!");
        }
    }
主线程 =====> 开始 =====> 1627895467578
异步线程 =====> 开始 =====> 1627895467635
主线程 =====> 结束 =====> 1627895469644
异步线程 =====> 结束 =====> 1627895472649

三、通过@Async注解

我们都知道,SpringBoot项目有一个的很重要的特点就是的注解化。如果你的项目是SpringBoot,那就又多了一种选择——@Async注解。

使用起来也非常简单,将要异步执行的代码封装成一个方法,然后用@Async注解该方法,然后在主方法中直接调用就行。

有以下两点需要注意:

  1. 类似@Tranctional注解,@Async注解的方法与调用方法不能在同一个类中,否则不生效
  2. JUnit框架的设计不考虑多线程场景,所以主线程退出后,子线程也会跟着立即退出,所以可以在后面加多线程休眠时间来观察异步线程的执行情况
@Test
public void mainThread() throws Exception{
    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());
    collectionBill.asyncThread();
    Thread.sleep(2000);
    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());
    Thread.sleep(4000); // 用于防止jvm停止,导致异步线程中断
}
@Async
public void asyncThread(){
    System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
    try{
        Thread.sleep(5000);
    }catch (InterruptedException e){
        e.printStackTrace();
    }
    System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
}

结果如上

四、通过CompletableFuture

CompletableFuture是JDK1.8的新特性,是对Future的扩展。如上2.2的future方式的异步编程仅仅能满足基本的需要,稍微复杂的一些异步处理Future接口似乎就有点束手无策了,而CompletableFuture实现了CompletionStage接口和Future接口,增加了异步回调、流式处理、多个Future组合处理的能力。

public static void main(String[] args) throws Exception{

    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    CompletableFuture.runAsync(() ->{
        System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
    },executorService);
    executorService.shutdown(); // 回收线程池
    Thread.sleep(2000);
    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());
}

4.1 使用构造函数创建CompletableFuture

public static class test{
        public static String getTestResult()
        {
            int i = 10/0;
            return "test";
        }
    }
    public static void main(String[] args) {

        CompletableFuture<String> completableFuture  = new CompletableFuture();
        new Thread(()->{
            try {
                completableFuture.complete(test.getTestResult());
            } catch (Exception e) {
                System.out.println("get exception in side");
                completableFuture.completeExceptionally(e);
            }
        }).start();

        try {
            String result = completableFuture.get();
            System.out.println(result);
        } catch (Exception e) {
            System.out.println("get exception out side");
            e.printStackTrace();
        }
    }

一般需要complete() 设置异步线程可能返回的值,以及completeExceptionally() 向上抛出异常

1)客户端可以使用重载版本的get方法,它使用一个超时参数来避免发生这样的情况.future.get(10, TimeUnit.SECONDS)

2)使用 CompletableFuture的completeExceptionally方法将导致CompletableFuture内发生问 题的异常抛出

4.2 使用工厂方法创建CompletableFuture

CompletableFuture提供了四个静态方法用来创建CompletableFuture对象

//异步操作无返回值
public static CompletableFuture<Void>   runAsync(Runnable runnable)
//使用指定线程池
public static CompletableFuture<Void>   runAsync(Runnable runnable, Executor executor)
//异步操作有返回值
public static <U> CompletableFuture<U>  supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U>  supplyAsync(Supplier<U> supplier, Executor executor)
  • Asynsc表示异步,而supplyAsync与runAsync不同在与前者异步返回一个结果,后者是void,这点通过看Runnable参数也是显而易见的
  • 第二个函数第二个参数表示是用我们自己创建的线程池,否则采用默认的ForkJoinPool.commonPool()作为它的线程池
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{return "hello world";});
System.out.println(future.get());  //阻塞的获取结果  ''helllo world"

4.3 主动计算

对于异步的CompletableFuture有以下四个方法获取结果

public T  get()
//设置超时时间
public T    get(long timeout, TimeUnit unit)
//如果结果当前已经计算完则返回结果或者抛出异常,否则返回给定的valueIfAbsent值,不会阻塞
public T    getNow(T valueIfAbsent)
//join()返回计算的结果或者抛出一个unchecked异常(CompletionException)
public T    join()

join()返回计算的结果或者抛出一个unchecked异常(CompletionException)

4.4. 对多个异步任务进行流水线操作

java8 流式处理在CompletableFuture 也得到了完美的体现

4.4.1 计算结果完成时的处理

当CompletableFuture的计算结果完成,或者抛出异常的时候,我们可以执行特定的操作

//和之前的CompleteableFuture使用相同的线程
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
//新选线程
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
//指定线程池
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
//exceptionally方法返回一个新的CompletableFuture,当原始的CompletableFuture抛出异常的时候,就会触发这个CompletableFuture的计算
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)

可以看到Action的类型是BiConsumer<? super T,? super Throwable>,它可以处理正常的计算结果,或者异常情况

方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其它的线程去执行(如果使用相同的线程池,也可能会被同一个线程选中执行)

这几个方法都会返回CompletableFuture,当Action执行完毕后它的结果返回原始的CompletableFuture的计算结果或者返回异常

public class Main {
    private static Random rand = new Random();
    private static long t = System.currentTimeMillis();
    static int getMoreData() {
        System.out.println("begin to start compute");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("end to start compute. passed " + (System.currentTimeMillis() - t)/1000 + " seconds");
        return rand.nextInt(1000);
    }
    public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(Main::getMoreData);
        Future<Integer> f = future.whenComplete((v, e) -> {
            System.out.println(v);
            System.out.println(e);
        });
        System.out.println(f.get());
        System.in.read();
    }
}

4.4.2 转换

如果我想转变计算结果的类型怎么办?
下面一组方法虽然也返回CompletableFuture对象,但是对象的值和原来的CompletableFuture计算的值不同,它的功能相当于将CompletableFuture转换成CompletableFuture

public <U> CompletableFuture<U> handle(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T,Throwable,? extends U> fn, Executor executor)

可以看到handle会传递异常,也有下面的操作忽略对异常的处理

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 100;
});
CompletableFuture<String> f =  future.thenApplyAsync(i -> i * 10).thenApply(i -> i.toString());
System.out.println(f.get()); //"1000"

它们与handle方法的区别在于handle方法会处理正常计算值和异常,因此它可以屏蔽异常,避免异常继续抛出。而thenApply方法只是用来处理正常值,因此一旦有异常就会抛出

4.4.3 纯消费

上面的方法是当计算完成的时候,会生成新的计算结果(thenApply, handle),或者返回同样的计算结果whenComplete
CompletableFuture还提供了一种处理结果的方法,只对结果执行Action,而不返回新的计算值,因此计算值为Void

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)

看它的参数类型也显而易见,它们是函数式接口Consumer,这个接口只有输入,没有返回值

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 100;
});
CompletableFuture<Void> f =  future.thenAccept(System.out::println);
System.out.println(f.get());

4.4.4 辅助方法 allOf 和 anyOf

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

allOf方法是当所有的CompletableFuture都执行完后执行计算。

anyOf方法是当任意一个CompletableFuture执行完后就会执行计算,计算的结果相同。

他们接受数组类型的参数

CompletableFuture [] futures = {CompletableFuture.supplyAsync(()->randomDelay()),CompletableFuture.supplyAsync(()->randomDelay()),CompletableFuture.supplyAsync(()->randomDelay())};

CompletableFuture.allOf(futures).join();

 

posted @   封-封  阅读(969)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示