Supplier 惰性调用和 Future#get 同步等待调用结合

📜Supplier 惰性调用和 Future#get 阻塞调用结合

本文是一篇探索异步结果值的文章。

知识点:异步结果、异步回调、Supplier、CompletableFuture 等。

📖一、背景介绍

关于任务异步执行,两个点不可避免:异步结果和异步回调。而在我的工程中有这样一段代码:

使用 CompletableFuture 进行封装,可以异步执行,异步回调,通过 get() 等待异步任务的结果。

注:任何方法都可以封装成 CallbackTask 调用。

Java 复制代码
public static <R> Optional<R> invokeWithResult(CallbackTask<R> executeTask) throws Exception {
      CompletableFuture<R> invoke = CompletableFuture.supplyAsync(() -> {
          // 异步执行
          return executeTask.execute();
      }).whenComplete((result, throwable) -> {
          // 执行结束后处理
          executeTask.onSuccess(result);
      }).exceptionally(throwable -> {
          // 失败时候处理
          executeTask.onFailure(throwable);
          return null;
      });

      // 获取异步调用结果
      return Optional.ofNullable(invoke.get(DEFAULT_MAX_TIMEOUT, TimeUnit.SECONDS));
  }

但用着用着就发现明显的不对劲,主线程会阻塞等待结果。异步转同步!

Optional.ofNullable(invoke.get(DEFAULT_MAX_TIMEOUT, TimeUnit.SECONDS))。get() 方法会进行阻塞等待。invokeWithResult 封装得不是很理想

Java 复制代码
/**
 * Waits if necessary for this future to complete, and then
 * returns its result. (简言之:阻塞等待结果)
 */
public T get() throws InterruptedException, ExecutionException {
    Object r;
    return reportGet((r = result) == null ? waitingGet(true) : r);
}

有没有好的方法,不执行阻塞等待呢?

直接返回 CompletableFuture 就可以了,等主线程用到的时候,再调用 CompletableFuture#get获取。

但用 CompletableFuture 作为方法返回值,个人觉得语义并不很好,有没有其他方式呢,带着这个问题过了一天,终于有了灵感 -- supplier 惰性求值。

📒二、supplier 惰性

Supplier接口在 Java 中通常被用于实现惰性求值。Supplier的特点在于它的 get()方法,该方法没有输入参数,并在被调用时返回一个T类型的值。在Supplier 被定义时,并不会执行 get() 方法中的代码;只有在显式调用 get() 方法时,代码才会执行,并且生成并提供一个值。

Java 复制代码
@FunctionalInterface
public interface Supplier<T> {
    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

这允许代码在需要时才生成值,从而实现了惰性求值

因为Supplier推迟了执行,直到调用者准备好接收值,这可以提高性能,避免不必要的计算,尤其是在处理耗时的操作或创建资源密集型对象时。

一个简单的例子可以展示Supplier的惰性行为

Java 复制代码
Supplier<Double> randomSupplier = () -> Math.random(); // 定义Supplier,但不调用

// ... 在代码的其他部分,只有当你需要一个随机值时才调用Supplier
double randomValue = randomSupplier.get(); // 实际生成值的操作发生在这里

在这个例子中,定义了一个返回随机数的Supplier。虽然Supplier已经被定义,但直到调用.get()时,它才会执行并返回一个随机数。因此Supplier是按需生成值的,执行是惰性的。

灵感来了,尝试用 supplier 来解决上面的问题。

📑三、supplier 和 Future#get 结合

将 Future#get 用 supplier 表达,等需要异步结果的时候再从 supplier 中获取。

Java 复制代码
public static <R> Supplier<R> invokeWithSupplier(CallbackTask<R> executeTask) {
  return () -> {
      try {
          return doInvoker(executeTask).get(DEFAULT_MAX_TIMEOUT, TimeUnit.SECONDS);
      } catch (Exception exception) {
          LOGGER.error("get result error", exception);
          return null;
      }
  };
}
private static <R> CompletableFuture<R> doInvoker(CallbackTask<R> executeTask) {
  .....
}

没错,supplier 达到效果。主线程使用 invokeWithSupplier 时不再堵塞。只是有 supplier#get才会阻塞获得结果值。

但为了更符合业务语义,进一步将 supplier 进行包装,用包装类承载结果

Java 复制代码
public class AsyncResp<T> {
    public static AsyncResp of() {
        return new AsyncResp();
    }

    private Supplier<T> container;
    // 阻塞获取结果集,有效避免 null 情况。
    public Optional<T> getResultWithOptional() {
        return Optional.ofNullable(this.container)
                .map(s -> this.container.get());
    }
    // 阻塞获取结果集
    public <T> T getResult() {
        return (T) Optional.ofNullable(this.container)
                .map(s -> this.container.get())
                .orElse(null);
    }
    // 设置 supplier
    public AsyncResp<T> setResult(Supplier<T> supplier) {
        this.container = supplier;
        return this;
    }
}

用 AsyncResp 作为返回类型,最终方法如下:

Java 复制代码
public static <R> AsyncResp<R> invoke(CallbackTask<R> executeTask) {
    return AsyncResp.of().setResult(() -> {
        try {
            return doInvoker(executeTask).get(DEFAULT_MAX_TIMEOUT, TimeUnit.SECONDS);
        } catch (Exception exception) {
            LOGGER.error("get result exception", exception);
            return null;
        }
    });
}

AsyncResp 比 CompletableFuture 作为方法返回结果类型,语言个人感觉更好一些。

获取异步结果, 其实有很多方式。而 supplier 和 future#get 的结合只是其中一种。

后来回顾,AsyncResp 甚至可以对 CompletableFuture 进行包装,如下所示:

Java 复制代码
public class AsyncResult<T> {

    public static AsyncResp of() {
        return new AsyncResp();
    }

    /**
     * 惰性获取结果
     */
    private CompletableFuture<T> future;
    .....
    
    @SneakyThrows
    public <T> T getResult() {
        return (T) future.get();
}

真的是,条条道路通罗马,多一些思考发现代码其实还挺有趣的!

📚四、guava#ListenableFuture

关于异步回调,异步结果,用得较多的是 guava#ListenableFuture。

是很多底层框架都在用的方法,稍微改造一下。

Java 复制代码
public static <R> ListenableFuture<R> invokeTaskWithFuture(CallbackTask<R> executeTask) {
    return gPool.submit(() -> executeTask.execute());
    Futures.addCallback(future, new FutureCallback<R>() {
        @Override
        public void onSuccess(R r) {
            executeTask.onSuccess(r);
        }
        @Override
        public void onFailure(Throwable t) {
            executeTask.onFailure(t);
        }
    }, gPool);
}

注: guava执行方法异常会继续向上抛出异常;CompletableFuture方式不会继续向上抛出异常。

✒️五、最后总结

异步、异步回调、异步结果是非常常见的问题,应用的场景也特别多,工程中常备此类工具。对于 supplier 惰性能力,包装了 Future#get,觉得挺有意思的,兴奋了一晚上,决定把过程记录下来。

关于 supplier 的惰性能力其实还有很多不错的应用场景。可以多去挖掘,比如惰性加载等。

相关推荐
非 白39 分钟前
【Java】单例模式
java·笔记·单例模式
IDRSolutions_CN1 小时前
如何在 PDF 文件中嵌入自定义数据
java·经验分享·pdf·软件工程·团队开发
_风中无我。1 小时前
Spring的过滤器获取请求体中JSON参数,同时解决Controller获取不到请求体参数的问题。
java·spring·json
bing_1581 小时前
Spring Boot 中为什么 需要限流、降级和熔断?
java
ccm031 小时前
高效开发助手:深入了解Hutool工具库
java·g工具库
大脑经常闹风暴@小猿1 小时前
1.1 go环境搭建及基本使用
开发语言·后端·golang
雪落南城1 小时前
【Maven】maven加载不到包
java·maven
尚学教辅学习资料1 小时前
基于SpringBoot的美食分享平台+LW示例参考
spring boot·后端·美食
tekin4 小时前
Go、Java、Python、C/C++、PHP、Rust 语言全方位对比分析
java·c++·golang·编程语言对比·python 语言·php 语言·编程适用场景
Vitalia4 小时前
从零开始学 Rust:基本概念——变量、数据类型、函数、控制流
开发语言·后端·rust