Java异步编程

Java异步编程

什么是异步编程?响应式编程+NIO。

异步和同步的区别

img

该图是传统模式下IO线程的交互流程,可以看出IO是阻塞的,即BIO的运行模式。

接口A发起调用接口B后,这段时间什么事也不做,主线程一直等到接口B数据返回,然后才能进行其他操作,可想而知如果接口A调用的接口不止B的话,那么等待的时间也是递增的,而这期间CPU也要一直占用着,白白浪费资源。因此这种模式下的交互流程,大部分时间都浪费在了IO等待上

再看看异步模式的交互流程,即NIO方式:

img

接口A发起调用接口B的请求后就立即返回,而不用阻塞等待接口B响应,这样的好处就是http线程可以马上得到复用,接着处理下一个前端请求的任务,如果接口B处理完返回数据后,会有一个回调线程池处理真正的响应,即这种模式下我们的业务流程是http线程只处理请求,回调线程处理接口响应

nio模式下虽然http线程和回调线程Asf的运行时间都很短,但是从http线程开始到asf回调处理完返回给前端结果的时间和bio即同步模式下的时间差异不大(在相同逻辑下),并不是nio模式下服务响应的整体时间就会缩短,而是会提升CPU的利用率,因为CPU不会再阻塞等待(不可中断状态减少),这样CPU就能有更多的资源来处理其他的请求任务,相同单位时间内能处理更多的任务,所以nio模式带来的好处是:

  • 提升QPS(用更少的线程资源实现更高的并发能力)
  • 降低CPU负荷,提高利用率

NIO原理

img

结合上面的接口交互图可知,接口B通过网络返回数据给调用方(接口A)这一过程,对应底层实现就是网卡接收到返回数据后,通过自身的DMA(直接内存访问)将数据拷贝到内核缓冲区,这一步不需要CPU参与操作,也就是把原先CPU等待的事情交给了底层网卡去处理,这样CPU就可以专注于我们的应用程序即接口的内部的逻辑运算。

NIO in Java

nio在java里的实现主要是几个核心组件:channelbufferselector,这些主键结合起来实现了上面所讲的多路复用机制

img

响应式编程

1. 什么是响应式编程?他和传统的编程方式有什么区别?

响应式编程可以简单理解为收到某个事件或通知后采取的一系列动作。比如响应操作系统的网络数据通知,然后以回调的方式处理数据。

传统的命令式编程主要由:顺序、分支、循环等控制流来完成不同的行为

响应式编程的特点是:

  • 以逻辑为中心转换为以数据为中心
  • 从命令式到声明式的转换

Java.Util.Concurrent.Future

在Java使用nio后无法立即拿到真实的数据,而是先得到一个"future",可以理解为邮戳或快递单,为了获悉真正的数据我们需要不停的通过快递单号查询快递进度,所以J.U.C中的Future是Java对应不变成的第一个解决方案,通常配合线程池使用,伪代码:

ExecutorService executor = Executors.newCachedThreadPool(); // 线程池
Future<String> future = executor.submit(() ->{
    Thread.sleep(200); // 模拟接口调用,耗时200ms
    return "hello world";
});
// 在输出下面异步结果时主线程可以不阻塞的做其他事情
// TODO 其他业务逻辑

System.out.println("异步结果:"+future.get()); //主线程获取异步结果

Future的缺点很明显:

  • 无法方便得知任务何时完成
  • 无法方便获得任务结果
  • 在主线程获得任务结果会导致主线程阻塞

ListenableFuture

Google并发包下的listenableFuture对Java原生的future做了扩展,顾名思义就是使用监听器模式实现的回调机制,所以叫可监听的Future。

Futures.addCallback(listenableFuture, new FutureCallback<String>() {
    @Override
    public void onSuccess(String result) {
        System.out.println("异步结果:" + result);
    }

    @Override
    public void onFailure(Throwable t) {
        t.printStackTrace();
    }
}, executor);

回调机制的最大问题是:Callback Hell回调地狱

  • 代码的字面形式和其所表达的业务含义不匹配;
  • 业务的先后关系在代码层面变成了包含和被包含的关系;
  • 大量使用Callback机制,使应该是先后的业务逻辑在代码形式上表现为层层嵌套,这会导致代码难以理解和维护。

如何结局Callback Hell问题呢?

响应式编程

主要是以一下两种解决方式:

  • 事件驱动机制
  • 链式调用(Lambda)

CompletableFuture

Java8里面的CompletableFuture和Java9里面的Flow Api勉强算是解决方案:

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() ->
    "hello"
);
// f2依赖f1的结果做转换
CompletableFuture<String> f2 = f1.thenApplyAsync(t ->
    t + " world"
);
System.out.println("异步结果:" + f2.get());

CompletableFuture处理简单的任务可以使用,但并不是一个完整的反应式编程方案,在服务调用复杂的情况下,存在服务编排、上下文传递、柔性限流(背压)方面的不足。

如果使用CompletableFuture面对这些问题时可能需要自己额外造一些轮子,Java9的Flow虽然是基于Reactive Streams规范实现的,但没有RxJava、Project Reactor这些异步框架丰富和强大的完整的解决方案。

当然如果接口比较简单,完全可以使用listebableFutureCompletableFuture

CompletableFuture常用方法:

supplyAsync:带返回值的异步查询

runAsync:无返回值的异步任务

thenApply:表示某个任务执行完之后执行的动作 也就是回调方法,将任务的执行结果传递到回调方法的参数

thenApplythenApplyAsync的区别:前者是同一个线程执行 后者是将第一个任务提交到线程池异步执行 实际执行的可能是另外一个线程

thenAccept: thenAccept和thenApply一样,但无返回值

thenReturn:对比thenApply没有入参也没有返回值

exceptionally:指定某个任务执行异常的回调方法,会将抛出的异常作为参数传递到该回调方法中

whenComplete:当某个任务执行完成后执行的回调方法(参数1:正常执行的结果 参数2:上个任务抛出的异常) 返回值类型继承上个任务

handle:对比whenComplete的区别是返回值类型可以自定义

thenCombine:将2个future组合起来,只有当2个都正常执行完了才会执行某个任务,将2个任务的执行结果作为入参,有返回值

thenAcceptBoth:对比thenCombine,无返回值

runAfterBoth:对比thenCombine,无入参,无返回值

allof:组合多个future 等待所有任务完成 返回值void

anyof: 对比allof,只要有一个future结束就可以做接下来的事情

get:等待任务完成(主线程休眠等待子任务完成 子线程执行完成后唤醒主线程) 抛出的是经过检查的异常 需要手动处理

join:对比get 会抛出的是未经过检查的异常,get不会抛出异常。

complete:如果尚未完成,则将 get() 和相关方法返回的值设置为给定值。参数: value - 结果值 返回:如果此调用导致此 CompletableFuture 转换为已完成状态,则为 true,否则为 false。

虽然方法很多但有个特征

  1. 以Async结尾的方法签名表示是在异步线程里执行,没有以Async结尾的方法则是由主线程调用;

  2. 如果参数里有Runnable类型,则没有返回结果,即纯消费的方法;

  3. 如果参数里没有指定executor则默认使用forkJoinPool线程池,指定了则以指定的线程池来执行任务;

JDK8中搭建CompletableFuture超时控制轮子

  1. JDK9源码实现:

    package com.yeshen.appcenter.util;
    import java.util.concurrent.*;
    import java.util.function.Function;
    
    /**
     * @Date 2022/1/20 20:07
     * @Created by YangGuo
     * @Description TODO
     * @Version 1.0
     */
    /**
     * java8中CompletableFuture异步处理超时的方法
     *
     * Java 8 的 CompletableFuture 并没有 timeout 机制,虽然可以在 get 的时候指定 timeout,是一个同步堵塞的操作。怎样让 timeout 也是异步的呢?Java 8 内有内建的机
     *  制支持,一般的实现方案是启动一个 ScheduledThreadpoolExecutor 线程在 timeout 时间后直接调用 CompletableFuture.completeExceptionally(new TimeoutException()),
     *  然后用acceptEither() 或者 applyToEither 看是先计算完成还是先超时:
     *
     *  在 java 9 引入了 orTimeout 和 completeOnTimeOut 两个方法支持 异步 timeout 机制:
     *
     * public CompletableFuture orTimeout(long timeout, TimeUnit unit) : completes the CompletableFuture with a TimeoutException after the specified timeout has elapsed.
     * public CompletableFuture completeOnTimeout(T value, long timeout, TimeUnit unit) : provides a default value in the case that the CompletableFuture pipeline times out.
     * 内部实现上跟我们上面的实现方案是一模一样的,只是现在不需要自己实现了。
     *
     * 实际上hystrix等熔断的框架,其实现线程Timeout之后就关闭线程,也是基于同样的道理,所以我们可以看到hystrix中会有一个Timer Thread
     *
     *
     */
    public class CompletableFutureTimeout {
        /**
         * Singleton delay scheduler, used only for starting and * cancelling tasks.
         */
        static final class Delayer {
            static ScheduledFuture<?> delay(Runnable command, long delay,
                                            TimeUnit unit) {
                return delayer.schedule(command, delay, unit);
            }
    
            static final class DaemonThreadFactory implements ThreadFactory {
                @Override
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(r);
                    t.setDaemon(true);
                    t.setName("CompletableFutureDelayScheduler");
                    return t;
                }
            }
    
            static final ScheduledThreadPoolExecutor delayer;
    
            // 注意,这里使用一个线程就可以搞定 因为这个线程并不真的执行请求 而是仅仅抛出一个异常
            static {
                (delayer = new ScheduledThreadPoolExecutor(
                        1, new CompletableFutureTimeout.Delayer.DaemonThreadFactory())).
                        setRemoveOnCancelPolicy(true);
            }
        }
    
        public static <T> CompletableFuture<T> timeoutAfter(long timeout, TimeUnit unit) {
            CompletableFuture<T> result = new CompletableFuture<T>();
            // timeout 时间后 抛出TimeoutException 类似于sentinel / watcher
            CompletableFutureTimeout.Delayer.delayer.schedule(() -> result.completeExceptionally(new TimeoutException()), timeout, unit);
            return result;
        }
    
        /**
         * 哪个先完成 就apply哪一个结果 这是一个关键的API,exceptionally出现异常后返回默认值
         *
         * @param t
         * @param future
         * @param timeout
         * @param unit
         * @param <T>
         * @return
         */
        public static <T> CompletableFuture<T> completeOnTimeout(T t, CompletableFuture<T> future, long timeout, TimeUnit unit) {
            final CompletableFuture<T> timeoutFuture = timeoutAfter(timeout, unit);
            return future.applyToEither(timeoutFuture, Function.identity()).exceptionally((throwable) -> t);
        }
    
        /**
         * 哪个先完成 就apply哪一个结果 这是一个关键的API,不设置默认值,超时后抛出异常
         *
         * @param t
         * @param future
         * @param timeout
         * @param unit
         * @param <T>
         * @return
         */
        public static <T> CompletableFuture<T> orTimeout(T t, CompletableFuture<T> future, long timeout, TimeUnit unit) {
            final CompletableFuture<T> timeoutFuture = timeoutAfter(timeout, unit);
            return future.applyToEither(timeoutFuture, Function.identity()).exceptionally((throwable) -> t);
        }
    }
    
  2. 自我简化实现

    /**
     * @Date 2022/1/20 18:03
     * @Created by YangGuo
     * @Description 工具类,JDK8中执行异步操作时,没有默认的超时设置选项,该工具类实现该功能
     * @Version 1.0
     */
    @Slf4j
    public class FutureAsyncTimeout {
        /**
         * 配置一个单例ScheduledThreadPoolExecutor,用于运行超时线程
         * 注意,这里使用一个线程就可以搞定 因为这个线程并不真的执行请求 而是仅仅抛出一个异常
         */
        private static final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
        static{
            scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
        }
    
        /**
         * 新建一个超时异步任务,超时则返回默认值,并中断已超时的异步任务。获取异步任务的结果,需要调用返回对象的get()方法。
         * @param supplier 异步任务
         * @param executor 异步任务使用的线程池
         * @param timeOut 超时时间
         * @param unit 时间单位
         * @param def 默认值
         * @param <T> 异步任务的返回类型
         * @return CompletableFuture对象,可以同该对象的其它方法一起使用
         * @throws ExecutionException
         * @throws InterruptedException
         */
        public static final <T> CompletableFuture<T> supplyAsync(final Supplier<T> supplier, Executor executor, long timeOut, TimeUnit unit, T def) throws ExecutionException, InterruptedException {
            CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier,executor);//根据异步任务创建Future对象
            CompletableFuture<T> timeTask = new CompletableFuture<T>();//创建超时Future对象
            //在单例线程池中延迟执行超时异步任务,指定延时时间后执行对应任务。completeExceptionally方法的作用:调用get()之后,返回指定的异常
            scheduledThreadPoolExecutor.schedule(() -> timeTask.completeExceptionally(new TimeoutException()), timeOut, unit);
            //核心方法:applyToEither将返回调用对象与参数对象中先执行完成的CompletableFuture,取得先执行完的返回值去参数2的action执行。
            //exceptionally方法作用:当前CompletableFuture以异常的方式结束执行时,将执行该方法指定的方法,也就是返回默认值。
            return future.applyToEither(timeTask, Function.identity()).exceptionally(t -> def);
        }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值