当前位置: 首页 > news >正文

【并发篇】CompletableFuture学习

CompletableFuture 异步编程

前言

我们异步执行一个任务时,一般是用线程池 Executor 去创建。

  • 如果不需要有返回值,任务实现 Runnable 接口;
  • 如果需要有返回值,任务实现 Callable 接口,调用 Executor 的 submit 方法,再使用 Future 获取即可。

如果多个线程存在依赖组合的话,我们怎么处理呢?

  • 可使用同步组件 CountDownLatch、CyclicBarrier 等,但是比较麻烦。
  • 其实有简单的方法,就是用 CompeletableFuture。

在现代的软件开发中,处理并发和异步任务变得越来越重要。Java 中的 CompletableFuture 类为我们提供了一种强大的方式来处理异步编程,让我们能够更有效地利用多核处理器和并行执行。

源码解析

源码:

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}

从源码可以看出 CompletableFuture 同时实现了 FutureCompletionStage 接口。

CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程的能力。

Future 接口有 5 个方法:

  1. boolean cancel(boolean mayInterruptIfRunning):尝试取消执行任务。
  2. boolean isCancelled():判断任务是否被取消。
  3. boolean isDone():判断任务是否已经被执行完成。
  4. get():等待任务执行完成并获取运算结果。
  5. get(long timeout, TimeUnit unit):多了一个超时时间。
/*** 表示一个异步计算的结果。该接口提供方法来检查计算是否完成、等待计算完成以及获取计算结果。* 一旦计算完成(无论是正常完成、异常终止还是被取消),则不能再次改变其状态。** @param <V> 计算结果的类型;如果计算不返回结果,则使用 {@code Void} 类型。*/
public interface Future<V> {/*** 尝试取消任务的执行。如果任务已经完成、已经被取消,或者由于某些原因无法取消,则此方法将不会产生任何效果。* 如果调用时任务尚未开始,则该任务不应启动。如果任务已经开始,则 mayInterruptIfRunning 参数决定是否应该尝试中断正在运行的任务。** @param mayInterruptIfRunning 如果为 true 并且任务已经开始执行,则会尝试中断任务。如果为 false,则任务允许继续运行直到完成。* @return 如果任务在调用此方法之前未完成,则返回 true;否则返回 false。*/boolean cancel(boolean mayInterruptIfRunning);/*** 判断任务是否在它正常完成前被取消。注意,只有当任务确实被取消时才会返回 true。* * @return 如果任务被取消则返回 true;否则返回 false。*/boolean isCancelled();/*** 判断任务是否已经完成。注意,这可能是因为任务正常结束、异常结束或被取消。** @return 如果任务已完成则返回 true;否则返回 false。*/boolean isDone();/*** 如果任务已完成,则返回任务的结果。如果任务尚未完成,则此方法将会阻塞,直到任务完成。* 如果任务被取消,则抛出 CancellationException 异常;如果任务异常完成,则抛出 ExecutionException 异常。** @return 任务的结果值。* @throws CancellationException 如果任务被取消。* @throws ExecutionException 如果计算过程中抛出了异常。* @throws InterruptedException 如果当前线程在等待时被中断。*/V get() throws InterruptedException, ExecutionException;/*** 如果任务在指定的超时时间内完成,则返回任务的结果。如果任务尚未完成,则此方法将会阻塞,直到任务完成或超时发生为止。* 如果任务被取消,则抛出 CancellationException 异常;如果任务异常完成,则抛出 ExecutionException 异常。* 如果超时时间到达而任务仍未完成,则抛出 TimeoutException 异常。** @param timeout 等待任务完成的最大时间。* @param unit    timeout 参数的时间单位。* @return 任务的结果值。* @throws CancellationException 如果任务被取消。* @throws ExecutionException 如果计算过程中抛出了异常。* @throws InterruptedException 如果当前线程在等待时被中断。* @throws TimeoutException 如果在给定的超时时间内未能完成任务。*/V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
}

CompletionStage 接口

CompletionStage 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。(大量使用了函数式编程)

什么是 CompletableFuture?

CompletableFuture 是 Java 8 引入的一个类,用于支持异步编程和操作多个异步任务。它是 Future 的扩展,提供了更多的功能和灵活性。通过 CompletableFuture,我们可以将多个异步任务串行或并行执行,然后等待它们的完成结果。

使用步骤

一、创建 CompletableFuture

常见的有两种方法

  1. 通过 new 关键字
  2. 基于 CompletableFuture 自带的静态工厂方法:runAsync()supplyAsync()
1、new 关键字

通过 new 关键字创建 CompletableFuture 对象这种使用方式可以看作是将 CompletableFuture 当做 Future 来使用。

举例:

创建异步运算的载体

CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();

上面代码创建了一个结果值类型为 RpcResponse<Object>CompletableFuture,你可以把 resultFuture 看作是异步运算结果的载体。

传入运算结果

// complete() 方法只能调用一次,后续调用将被忽略。
resultFuture.complete(rpcResponse);

假设在未来的某个时刻,我们得到了最终的结果。这时,我们可以调用 complete() 方法为其传入结果,这表示 resultFuture 已经被完成了。

判断任务是否已经被完成

public boolean isDone() {return result != null;
}

可以通过 isDone() 方法来检查是否已经完成。(Future 接口的方法)

等待任务执行完成并获取运算结果

rpcResponse = completableFuture.get();

可以通过调用 get() 方法来获取异步计算结果。调用 get() 方法的线程会阻塞直到 CompletableFuture 完成运算。(阻塞等待)

如果你已经知道计算的结果的话,可以使用静态方法 completedFuture() 来创建 CompletableFuture

CompletableFuture<String> future = CompletableFuture.completedFuture("hello!");
assertEquals("hello!", future.get());

completedFuture() 方法底层调用的是带参数的 new 方法,只不过,这个方法不对外暴露。

public static <U> CompletableFuture<U> completedFuture(U value) {return new CompletableFuture<U>((value == null) ? NIL : value);
}
代码示例
    /*** new 关键字创建 CompletableFuture* 利用 complete 方法手动完成 CompletableFuture*/@Testvoid testNew() throws ExecutionException, InterruptedException {// 1、创建一个新的未完成的 CompletableFutureCompletableFuture<String> future = new CompletableFuture<>();// 模拟异步操作完成后手动完成 CompletableFutureString expectedResult = "Hello, World!";future.complete(expectedResult);// 测试是否成功完成并返回预期结果assertEquals(expectedResult, future.get());// 2、测试异常完成的情况CompletableFuture<Void> failingFuture = new CompletableFuture<>();RuntimeException expectedException = new RuntimeException("Oops!");failingFuture.completeExceptionally(expectedException);try {failingFuture.get();fail("Expected exception not thrown");} catch (ExecutionException e) {assertInstanceOf(RuntimeException.class, e.getCause());assertEquals(expectedException.getMessage(), e.getCause().getMessage());}}/*** 静态方法 completedFuture 创建一个已完成的 CompletableFuture* 底层用的也是 new*/@Testvoid testCompletedFuture() throws ExecutionException, InterruptedException {CompletableFuture<String> future = CompletableFuture.completedFuture("hello!");assertEquals("hello!", future.get());}
2、静态工厂方法
  1. supplyAsync 执行 CompletableFuture 任务,支持返回值
  2. runAsync 执行 CompletableFuture 任务,没有返回值。因为 runAsync() 方法接受的参数是 Runnable ,这是一个函数式接口,不允许返回值。
supplyAsync 方法
// 使用默认内置线程池ForkJoinPool.commonPool(),根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)// 使用自定义线程池,根据supplier构建执行任务(推荐)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

supplyAsync() 方法接受的参数是 Supplier<U> ,是一个函数式接口,U 是返回结果值的类型。

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

使用场景:当你需要异步操作且关心返回结果的时候,可以使用 supplyAsync() 方法。

runAsync 方法
// 使用默认内置线程池ForkJoinPool.commonPool(),根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable) // 使用自定义线程池,根据runnable构建执行任务(推荐)
public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor)

使用场景:当你需要异步操作且不关心返回结果的时候可以使用 runAsync() 方法。

@FunctionalInterface
public interface Runnable {public abstract void run();
}
代码示例
    /*** supplyAsync 和 runAsync 的区别* supplyAsync 支持返回值* runAsync    不支持返回值*/@Testvoid testSupplyAsyncAndRunAsync() throws ExecutionException, InterruptedException {CompletableFuture<Void> runFuture = CompletableFuture.runAsync(() -> System.out.println("hello runAsync!"));// 控制台输出 "hello!"runFuture.get();CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> "hello supplyAsync!");// 控制台 不会 输出 "hello!"supplyFuture.get();// 进行断言,判断返回值是否为 "hello!",不通过就会抛出异常assertEquals("hello supplyAsync!", supplyFuture.get());}/*** 自定义线程池写法*/@Testvoid testSupplyAsyncAndRunAsync2() {// 自定义线程池ExecutorService executor = Executors.newCachedThreadPool();// runAsync的使用CompletableFuture<Void> runFuture = CompletableFuture.runAsync(() -> System.out.println("run, cmty256"), executor);// supplyAsync的使用CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> {System.out.println("supply, cmty256");return "cmty256";}, executor);System.out.println("=============================异步操作,输出顺序不定=============================");// runAsync的future没有返回值,输出nullSystem.out.println(runFuture.join());System.out.println("=============================异步操作,输出顺序不定=============================");// supplyAsync的future,有返回值System.out.println(supplyFuture.join());executor.shutdown(); // 线程池需要关闭}

二、简单任务异步回调

image

处理异步结算结果

当我们获取到异步计算的结果之后,还可以对其进行进一步的处理,比较常用的方法有下面几个:

  1. thenRun() / thenRunAsync()
  2. thenAccept() / thenAcceptAsync()
  3. thenApply() / thenApplyAsync()
  4. whenComplete()

thenRun 和 thenRunAsync 有什么区别?

源码

    private static final 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);}

如果你执行第一个任务的时候,传入了一个自定义线程池:

  • 调用 thenRun 方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池
  • 调用 thenRunAsync 执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是 ForkJoin 线程池

tips: thenAccept 和 thenAcceptAsync,thenApply 和 thenApplyAsync 等,它们之间的区别也是这个。

thenRun / thenRunAsync

CompletableFuture 的 thenRun 方法,

  • 通俗点讲就是,做完第一个任务后,再做第二个任务。
  • 某个任务执行完成后,执行回调方法;
  • 但是前后两个任务没有参数传递,第二个任务也没有返回值
public CompletableFuture<Void> thenRun(Runnable action);public CompletableFuture<Void> thenRunAsync(Runnable action);

代码示例:

    /*** thenRun()方法* <p>* 做完第一个任务后,再做第二个任务* 但是前后两个任务没有参数传递,第二个任务也没有返回值。* </p>*/@Testvoid testThenRun() throws ExecutionException, InterruptedException {CompletableFuture<String> firstFuture = CompletableFuture.supplyAsync(() -> {System.out.println("先执行第一个CompletableFuture方法任务");return "沉梦听雨";});CompletableFuture<Void> thenRunFuture = firstFuture.thenRun(() -> {System.out.println("thenRun-接着执行第二个任务");});System.out.println("返回值:" + thenRunFuture.get());// 输出/*先执行第一个CompletableFuture方法任务thenRun-接着执行第二个任务返回值:null*/}
thenAccept / thenAcceptAsync

CompletableFuture 的 thenAccept 方法表示,

  • 第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,
  • 但是回调方法是没有返回值的。

代码示例:

    /*** thenAccept()方法* <p>* 做完第一个任务后,再做第二个任务* 可以接收入参,但是没有返回值。* </p>*/@Testvoid testThenAccept() throws ExecutionException, InterruptedException {CompletableFuture<String> firstFuture = CompletableFuture.supplyAsync(() -> {System.out.println("第一个CompletableFuture方法任务");return "沉梦听雨";});CompletableFuture<Void> thenAcceptFuture = firstFuture.thenAccept((a) -> {if ("沉梦听雨".equals(a)) {System.out.println("入参校验成功");}System.out.println("thenAccept-接着执行第二个任务");});System.out.println("返回值:" + thenAcceptFuture.get());// 输出/*第一个CompletableFuture方法任务入参校验成功thenAccept-接着执行第二个任务返回值:null*/}
thenApply / thenApplyAsync

thenApply() 方法接收一个 Function 实例,用它来处理结果。

// 沿用上一个任务的线程池
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {return uniApplyStage(null, fn);
}// 使用默认的 ForkJoinPool 线程池(不推荐)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) {return uniApplyStage(defaultExecutor(), fn);
}
// 使用自定义线程池(推荐)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) {return uniApplyStage(screenExecutor(executor), fn);
}

代码示例:

CompletableFuture 的 thenApply 方法表示,

  • 第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,
  • 并且回调方法是有返回值的。
    /*** thenApply()方法* <p>* 做完第一个任务后,再做第二个任务* 可以接收入参,并且有返回值。* </p>*/@Testvoid testThenApply() throws ExecutionException, InterruptedException {CompletableFuture<String> firstFuture = CompletableFuture.supplyAsync(() -> {System.out.println("第一个CompletableFuture方法任务");return "cmty256";});CompletableFuture<String> thenApplyFuture = firstFuture.thenApply((a) -> {if ("沉梦听雨".equals(a)) {return "第一个任务的返回值";}return "thenApply-第二个任务的返回值";});System.out.println("返回值:" + thenApplyFuture.get());// 输出/*第一个CompletableFuture方法任务返回值:thenApply-第二个任务的返回值*/}
whenComplete

CompletableFuture 的 whenComplete 方法表示,

  • 某个任务执行完成后,执行的回调方法,无返回值
  • 并且 whenComplete 方法返回的 CompletableFuture 的 result 是上个任务的结果
    /*** whenComplete()方法* <p>* 两个任务在同一个线程中执行* 第二个任务可以接收入参* 第二个任务返回的是第一个任务的返回值* </p>*/@Testvoid testWhenComplete() throws ExecutionException, InterruptedException {CompletableFuture<String> firstFuture = CompletableFuture.supplyAsync(() -> {System.out.println("当前线程名称:" + Thread.currentThread().getName());try {Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}return "沉梦听雨";});CompletableFuture<String> whenCompleteFuture = firstFuture.whenComplete((a, throwable) -> {System.out.println("当前线程名称:" + Thread.currentThread().getName());System.out.println("上个任务执行完啦,还把【" + a + "】传过来");if ("沉梦听雨".equals(a)) {System.out.println("入参校验成功");}System.out.println("whenComplete-接着执行第二个任务");});System.out.println("返回值:" + whenCompleteFuture.get());// 输出/*当前线程名称:ForkJoinPool.commonPool-worker-19当前线程名称:ForkJoinPool.commonPool-worker-19上个任务执行完啦,还把【沉梦听雨】传过来入参校验成功whenComplete-接着执行第二个任务返回值:沉梦听雨*/}
异常处理

异步操作可能会失败,CompletableFuture 允许我们使用 exceptionally()handle() 方法来处理异步操作的异常。

handle

主要用于处理异步任务的结果异常

  • 如果任务正常完成,则返回任务的结果;
  • 如果任务抛出异常,则可以指定一个默认值或其他处理逻辑。
public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) {return uniHandleStage(null, fn);
}public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) {return uniHandleStage(defaultExecutor(), fn);
}public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) {return uniHandleStage(screenExecutor(executor), fn);
}

代码示例:

    /*** handle()方法* <p>* 该方法用于处理异步任务的结果或异常。* - 如果任务正常完成,则返回任务的结果;* - 如果任务抛出异常,则可以指定一个默认值或其他处理逻辑。* 在此示例中,异步任务会抛出一个 RuntimeException,* 而 handle() 方法会捕获该异常并返回一个默认字符串 "world!"。* </p>*/@Testvoid testHandle() throws ExecutionException, InterruptedException {CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {throw new RuntimeException("Computation error!");// return "world!";}).handle((res, ex) -> {// res 代表返回的结果// ex 的类型为 Throwable,代表抛出的异常if (ex != null) {// 异常被捕获: java.lang.RuntimeException: Computation error!System.out.println("异常被捕获: " + ex.getMessage());return "world!";}return (String) res;// return (String) (res != null ? res : "world!");});assertEquals("world!", future.get());}
exceptionally
  • 主要用于处理异步任务中发生的异常
    /*** exceptionally() 方法* <p>* 该方法用于处理异步任务中发生的异常。* 如果任务抛出异常,则可以指定一个默认值或其他处理逻辑。* 在此示例中,异步任务会抛出一个 RuntimeException,* 而 exceptionally() 方法会捕获该异常并返回一个默认字符串 "world!"。* </p>*/@Testvoid testExceptionally() throws ExecutionException, InterruptedException {CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {throw new RuntimeException("Computation error!");}).exceptionally(ex -> {// java.util.concurrent.CompletionException: java.lang.RuntimeException: Computation error!System.out.println(ex.toString());// 返回默认值 "world!"return "world!";});assertEquals("world!", future.get());}
completeExceptionally

设置 CompletableFuture 的结果就是异常

  • 可以使用 completeExceptionally() 方法为其赋值。
  • 当你需要手动控制 CompletableFuture 的状态,并且希望在某些条件下将其标记为异常完成时,可以使用 completeExceptionally()
    /*** completeExceptionally() 方法* <p>* 该方法用于手动将 CompletableFuture 标记为异常完成状态。* 在此示例中,我们创建了一个 CompletableFuture 对象,并使用 completeExceptionally() 方法手动设置一个异常。* 然后,调用 get() 方法会抛出该异常。* </p>*/@Testvoid testCompleteExceptionally() throws InterruptedException {CompletableFuture<String> completableFuture = new CompletableFuture<>();// 手动设置 CompletableFuture 为异常完成状态completableFuture.completeExceptionally(new RuntimeException("Calculation failed!"));try {// 直接 get() 会抛出异常completableFuture.get();} catch (ExecutionException e) {// 捕获到异常: Calculation failed!System.out.println("捕获到异常: " + e.getCause().getMessage());}}

三、多个任务组合处理

AND 组合关系

thenCombine / thenAcceptBoth / runAfterBoth 都表示:

  • 将两个 CompletableFuture 组合起来,只有这两个都正常执行完了,才会执行某个任务

区别在于:

  • thenCombine:会将两个任务的执行结果作为方法入参,传递到指定方法中,且有返回值
  • thenAcceptBoth: 会将两个任务的执行结果作为方法入参,传递到指定方法中,且无返回值
  • runAfterBoth 不会把执行结果当做方法入参,且没有返回值
thenCombine / thenCombineAsync

代码示例:

    /*** thenCombineAsync() 方法测试* <p>* 该方法用于组合两个异步任务的结果。* 在此示例中,我们创建了两个异步任务,并使用 thenCombineAsync() 方法将它们的结果组合在一起。* 第一个任务是一个已经完成的 CompletableFuture,第二个任务通过 supplyAsync() 方法异步执行。* 最终,两个任务的结果会被组合成一个新的字符串,并通过 join() 方法获取结果。* </p>*/@Testvoid testThenCombineAsync() {// 创建一个已经完成的 CompletableFuture,结果为 "第一个异步任务"CompletableFuture<String> firstFuture = CompletableFuture.completedFuture("第一个异步任务");// 创建一个固定大小的线程池ExecutorService executor = Executors.newFixedThreadPool(10);// 创建第二个异步任务,并使用 thenCombineAsync() 方法组合两个任务的结果CompletableFuture<String> future = CompletableFuture// 第二个异步任务.supplyAsync(() -> "第二个异步任务", executor)// 第三个任务,组合前两个任务的结果.thenCombineAsync(firstFuture, (s, other) -> {System.out.println(s); // 打印 supplyAsync 任务的结果System.out.println(other); // 打印 firstFuture 任务的结果return "两个异步任务的组合"; // 返回组合后的结果}, executor);// 获取并打印组合后的结果System.out.println(future.join());// 关闭线程池executor.shutdown();// 输出/*第二个异步任务第一个异步任务两个异步任务的组合*/}

源码分析:

    // 不能传入自定义线程池public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn) {return biApplyStage(null, other, fn);}// 不能传入自定义线程池public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn) {return biApplyStage(defaultExecutor(), other, fn);}// 可以传入自定义线程池public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn, Executor executor) {return biApplyStage(screenExecutor(executor), other, fn);}
allOf 全部执行完

所有任务都执行完成后,才执行 allOf 返回的 CompletableFuture。

  • 如果任意一个任务异常,allOf 的 CompletableFuture,执行 get 方法,会抛出异常。
    /*** allOf() 方法测试* <p>* 该方法用于组合多个 CompletableFuture 任务,确保所有任务都完成后才继续执行后续操作。* 在此示例中,我们创建了两个异步任务,并使用 allOf() 方法将它们组合在一起。* 当所有任务完成后,会执行 whenComplete() 方法中的回调,打印 "finish"。* </p>*/@Testvoid testAllOf() throws ExecutionException, InterruptedException {// 创建第一个异步任务,任务完成后打印 "我执行完了"CompletableFuture<Void> a = CompletableFuture.runAsync(() -> System.out.println("我执行完了"));// 创建第二个异步任务,任务完成后打印 "我也执行完了"CompletableFuture<Void> b = CompletableFuture.runAsync(() -> System.out.println("我也执行完了"));// 使用 CompletableFuture.allOf 组合两个异步任务,由于 runAsync 方法中的任务是异步执行的,具体的执行顺序可能会有所不同// 当所有任务完成后,执行 whenComplete 回调CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(a, b)// 回调函数中的参数 res 和 ex 分别表示结果和异常// 在 allOf 的情况下,res 为 null,ex 为可能的异常(如果没有异常则为 null).whenComplete((res, ex) -> System.out.println("finish"));// 为 nullallOfFuture.get();// 输出/*我也执行完了我执行完了finish*/}@Testvoid testAllOf2() {List<CompletableFuture<?>> futures = Arrays.asList(CompletableFuture.runAsync(() -> System.out.println("Task 1")),CompletableFuture.runAsync(() -> {// try {//     Thread.sleep(500); // 模拟较长时间的任务// } catch (InterruptedException e) {//     Thread.currentThread().interrupt();// }System.out.println("Task 2");}));// 等待所有任务完成CompletableFuture// 这里使用了 List 的 toArray(T[] a) 方法将 List<CompletableFuture<?>> 转换为 CompletableFuture<?>[] 数组。// 之所以传递一个空的 CompletableFuture[0] 是因为 toArray(T[] a) 方法需要一个与列表元素类型相同的数组作为参数,以确定返回数组的类型和大小。// 如果不提供任何数组作为参数,toArray() 方法将返回 Object[],这在调用 allOf() 时会导致编译错误,因为它期望的是 CompletableFuture<?>[].allOf(futures.toArray(new CompletableFuture[0])).join();// 输出(顺序不固定)/*Task 2Task 1*/}
anyOf 任一执行完

任意一个任务执行完,就执行 anyOf 返回的 CompletableFuture。

  • 如果执行的任务异常,anyOf 的 CompletableFuture,执行 get 方法,会抛出异常。
    /*** anyOf() 方法测试* <p>* 该方法用于组合多个 CompletableFuture 任务,只要其中任何一个任务完成,就会继续执行后续操作。* 在此示例中,我们创建了两个异步任务,并使用 anyOf() 方法将它们组合在一起。* 只要其中一个任务完成,就会执行 whenComplete() 方法中的回调,打印 "finish"。* </p>*/@Testvoid testAnyOf() {// 创建第一个异步任务,任务完成后打印 "我执行完了"// 该任务会休眠 3 秒钟CompletableFuture<Void> a = CompletableFuture.runAsync(() -> {try {Thread.sleep(3000L);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("我执行完了");});// 创建第二个异步任务,任务完成后打印 "我也执行完了"CompletableFuture<Void> b = CompletableFuture.runAsync(() -> System.out.println("我也执行完了"));// 使用 CompletableFuture.anyOf 组合两个异步任务// 只要其中一个任务完成,就会执行 whenComplete 回调CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(a, b).whenComplete((res, ex) -> System.out.println("finish"));// 等待任意一个给定的 CompletableFuture 完成anyOfFuture.join();// 输出(这里由于第一个任务会休眠 3 秒,所以一直会输出第一种情况)/*我也执行完了finish或者我执行完了finish*/}

join() 的含义是:

  • 等待一个异步操作(也就是 CompletableFuture)完成并获取其结果。

  • 具体来说,join 方法会阻塞当前线程,直到相应的 CompletableFuture 完成,并返回其计算结果(或异常)。如果在调用 join 时异步操作还未完成,那么当前线程将一直阻塞等待,直到操作完成或者抛出异常。

get() 和 join() 方法

CompletableFutureget() 方法和 join() 方法都是用来等待异步任务完成并获取其结果的,但它们在处理异常和返回类型上有不同的行为。

下面详细解释这两个方法的区别:

1、get() 方法

  • 签名V get() throws InterruptedException, ExecutionException;
  • 功能:阻塞当前线程直到 CompletableFuture 完成,并返回结果。
  • 异常处理
    • 如果任务被中断,则抛出 InterruptedException
    • 如果任务执行过程中抛出了异常,则封装为 ExecutionExceptionCompletionException 并抛出。
try {String result = future.get(); // 阻塞直到任务完成
} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态// 处理中断异常
} catch (ExecutionException e) {// 处理执行期间发生的异常
}

2. join() 方法

  • 签名V join();
  • 功能:类似于 get(),它也会阻塞当前线程直到 CompletableFuture 完成,但它不会抛出受检异常(checked exception),而是将所有异常都封装为未受检的 CompletionException
  • 异常处理
    • 如果任务正常完成,join() 返回任务的结果。
    • 如果任务因为异常而失败,join() 将抛出一个包含原始异常的 CompletionException
    • 不会抛出 InterruptedException,即使任务被中断;相反,它会继续等待直到任务完成或遇到其他类型的异常。
String result = future.join(); // 阻塞直到任务完成,不抛出受检异常

使用建议

  • 选择 get() 还是 join()

    • 如果你希望明确区分不同类型的异常并且需要处理 InterruptedException,那么应该使用 get() 方法。
    • 如果你更倾向于简化代码,不需要显式处理 InterruptedException,并且可以接受所有的异常都被封装为 CompletionException,那么可以使用 join() 方法。
  • 性能考虑

    • 在大多数情况下,两者之间的性能差异可以忽略不计,选择哪个方法主要取决于你的异常处理策略和个人偏好。

示例代码

这里有一个简单的例子来展示 get()join() 的用法以及它们如何处理异常:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;public class CompletableFutureExample {public static void main(String[] args) {// 创建一个可能会失败的 CompletableFutureCompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {if (Math.random() > 0.5) {throw new RuntimeException("Task failed!");}return "Success!";});try {// 使用 get() 方法System.out.println("Using get(): " + future.get());} catch (InterruptedException | ExecutionException e) {System.err.println("Error using get(): " + e.getCause());}// 使用 join() 方法try {System.out.println("Using join(): " + future.join());} catch (CompletionException e) {System.err.println("Error using join(): " + e.getCause());}}
}

在这个例子中,你可以看到当 CompletableFuture 因为异常而失败时,get()join() 方法都会捕获到异常,

  • 但是 get() 抛出了 ExecutionException
  • join() 则抛出了 CompletionException

学习参考

  • 异步编程利器:CompletableFuture详解 |Java 开发实战 - 掘金 (juejin.cn)
  • CompletableFuture 详解 | JavaGuide(Java面试 + 学习指南)

相关文章:

【并发篇】CompletableFuture学习

CompletableFuture 异步编程 前言 我们异步执行一个任务时&#xff0c;一般是用线程池 Executor 去创建。 如果不需要有返回值&#xff0c;任务实现 Runnable 接口&#xff1b;如果需要有返回值&#xff0c;任务实现 Callable 接口&#xff0c;调用 Executor 的 submit 方法…...

【动手学电机驱动】STM32-MBD(3)Simulink 状态机模型的部署

STM32-MBD&#xff08;1&#xff09;安装 Simulink STM32 硬件支持包 STM32-MBD&#xff08;2&#xff09;Simulink 模型部署入门 STM32-MBD&#xff08;3&#xff09;Simulink 状态机模型的部署 [STM32-MBD&#xff08;4&#xff09;Simulink 状态机实现按键控制] (https://bl…...

springCloudGateWay使用总结

1、什么是网关 功能: ①身份认证、权限验证 ②服务器路由、负载均衡 ③请求限流 2、gateway搭建 2.1、创建一个空项目 2.2、引入依赖 2.3、加配置 3、断言工厂 4、过滤工厂 5、全局过滤器 6、跨域问题...

04、Redis深入数据结构

一、简单动态字符串SDS 无论是Redis中的key还是value&#xff0c;其基础数据类型都是字符串。如&#xff0c;Hash型value的field与value的类型&#xff0c;List型&#xff0c;Set型&#xff0c;ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了…...

zephyr移植到STM32

Zephy如何移植到单片机 1. Window下搭建开发环境1.1 安装Choncolatey1.2 安装相关依赖1.3创建虚拟python环境1.4 安装west1.4.1 使用 pip 安装 west1.4.2 检查 west 安装路径1.4.3 将 Scripts路径添加到环境变量1.4.4 验证安装 1.5 获取zephyr源码和[安装python](https://so.cs…...

Windows使用AutoHotKey解决鼠标键连击现象(解决鼠标连击、单击变双击的故障)

注&#xff1a;罗技鼠标&#xff0c;使用久了之后会出现连击现象&#xff0c;如果刚好过保了&#xff0c;可以考虑使用软件方案解决连击现象&#xff1a; 以下是示例AutoHotKey脚本&#xff0c;实现了调用XButton1用于关闭窗口&#xff08;以及WinW&#xff0c;XButton2也导向…...

案例研究:UML用例图中的结账系统

在软件工程和系统分析中&#xff0c;统一建模语言&#xff08;UML&#xff09;用例图是一种强有力的工具&#xff0c;用于描述系统与其用户之间的交互。本文将通过一个具体的案例研究&#xff0c;详细解释UML用例图的关键概念&#xff0c;并说明其在设计结账系统中的应用。 用…...

将光源视角的深度贴图应用于摄像机视角的渲染

将光源视角的深度贴图应用于摄像机视角的渲染是阴影映射&#xff08;Shadow Mapping&#xff09;技术的核心步骤之一。这个过程涉及到将摄像机视角下的片段坐标转换到光源视角下&#xff0c;并使用深度贴图来判断这些片段是否处于阴影中。 1. 生成光源视角的深度贴图 首先&…...

安卓漏洞学习(十八):Android加固基本原理

APP加固技术发展历程 APK加固整体思路 加固整体思路&#xff1a;先解压apk文件&#xff0c;取出dex文件&#xff0c;对dex文件进行加密&#xff0c;然后组合壳中的dex文件&#xff08;Android类加载机制&#xff09;&#xff0c;结合之前的apk资源&#xff08;解压apk除dex以外…...

前端数据模拟器 mockjs 和 fakerjs

功能&#xff1a;帮助前端生成随机数据&#xff0c;独立于后端单独开发 一、mockjs 安装&#xff1a;npm install mockjs 优点&#xff1a;官网是中文。 缺点&#xff1a;目前该库已经无人维护&#xff0c;也没人解决github上的bug。 官网 github地址 二、fakerjs 安装&#xf…...

Ruby语言的软件开发工具

Ruby语言的软件开发工具概述 引言 Ruby是一种简单且功能强大的编程语言&#xff0c;它以优雅的语法和灵活性而闻名。自1995年首次发布以来&#xff0c;Ruby已经被广泛应用于各种开发领域&#xff0c;特别是Web开发。随着Ruby语言的普及&#xff0c;相关的开发工具也日益丰富。…...

P8772 [蓝桥杯 2022 省 A] 求和

题目描述 给定 &#x1d45b; 个整数 &#x1d44e;1,&#x1d44e;2,⋯ ,&#x1d44e;&#x1d45b; 求它们两两相乘再相加的和&#xff0c;即 &#x1d446;&#x1d44e;1⋅&#x1d44e;2&#x1d44e;1⋅&#x1d44e;3⋯&#x1d44e;1⋅&#x1d44e;&#x1d45b;&…...

(七)Linux库的串口开发

文章目录 基于官方提供的串口测试代码部分解析代码部分1. usage 函数2. opt_parsing_err_handle 函数3. sig_handle 函数4. init_serial 函数5. serial_write 函数6. serial_read 函数7. run_read_mode 函数8. run_write_mode 函数9. run_loopback_test 函数 进行测试第一步编译…...

【git】在服务器使用docker设置了一个gogs服务器,访问和现实都不理想

以下问题应该都可以通过设置custom/conf/app.ini来解决 配置文档参考地址:https://www.bookstack.cn/read/gogs_zh/advanced-configuration_cheat_sheet.md domain显示的事localhost&#xff0c;实际上应该是一个IP地址。 关键字&#xff1a; DOMAIN ROOT_URL 因为是docker…...

ubuntu报错:没有在该文件夹中粘贴文件的权限

1 现象&#xff1a; 近期给ubuntu扩展了硬盘&#xff0c;但是在其中进行文件操作时提示“没有在该文件夹中粘贴文件的权限” 2 原因&#xff1a; 新增硬盘挂载地址为“/home/username/data/” 终端输入 ls -ld /home/username/data/输出 drwxr-xr-x 3 root root 4096 1月…...

JavaWeb开发(六)XML介绍

1. XML介绍 1.1. 什么是XML &#xff08;1&#xff09;XML 指可扩展标记语言(EXtensible Markup Language)XML 是一种很像HTML的标记语言。   &#xff08;2&#xff09;XML 的设计宗旨是传输数据(目前主要是作为配置文件)&#xff0c;而不是显示数据。   &#xff08;3&a…...

Vue 3 和 Electron 来构建一个桌面端应用

我们将使用 Vue 3 和 Electron 来构建一个桌面端应用&#xff0c;该应用可以通过 Websocket 与服务器进行通信&#xff0c;并实现心跳检测、客户端上线、获取资产信息以及修改资产状态的功能。以下是实现步骤的概述&#xff1a; 项目结构&#xff1a;创建一个 Vue 3 项目&…...

Python中的asyncio:高效的异步编程模型

随着互联网应用的快速发展&#xff0c;程序的响应性和处理效率成为衡量系统性能的重要指标。传统的同步编程模型在面对高并发和IO密集型任务时&#xff0c;常常显得捉襟见肘&#xff0c;难以满足现代应用的需求。Python的asyncio库作为一种高效的异步编程模型&#xff0c;为开发…...

《解锁鸿蒙系统AI能力,开启智能应用开发新时代》

在当今科技飞速发展的时代&#xff0c;鸿蒙系统以其独特的分布式架构和强大的AI能力&#xff0c;为开发者们带来了前所未有的机遇。本文将深入探讨开发者如何利用鸿蒙系统的AI能力开发更智能的应用&#xff0c;开启智能应用开发的新时代。 鸿蒙系统构筑了15系统级的AI能力&…...

安卓OCR使用(Google ML Kit)

OCR是一个很常用的功能&#xff0c;Google ML Kit提供了OCR能力&#xff0c;用起来也很简单&#xff0c;本文介绍一下使用方法。 1. 相关概念 名词概念解释TextBlock块一个段落Line行一行文本Element元素单词&#xff1b;对汉字来说&#xff0c;类似"开头 (分隔符)中间&…...

使用redis的5种常用场景

文章目录 1. 缓存热点数据2. 分布式锁3. 计数器和限流器4. 消息队列5. 会话管理总结 在日常开发工作中&#xff0c;Redis作为一款高性能的内存数据库&#xff0c;凭借其强大的功能特性和卓越的性能表现&#xff0c;已经成为了许多项目中不可或缺的组件。本文将详细介绍Redis在实…...

Extreme670和440的DHCP和vlan划分

1.网关配置 防火墙 USG 添加静态路由&#xff0c;也就是回指路由192.168.0.0 255.255.0.0 192.168.100.2 usg关闭DHCP192.168.100.0段的&#xff0c;usg接口的网关地址是192.168.100.1&#xff0c;防火墙策略启用192.168.100.0段到wan1段的内网和外网的NAT地址转换。 2…...

VTK知识学习(33)-交互问题2

1、前言 主要是针对前面有过实现不了交互的情况进行说明&#xff0c;经过一些尝试和分析调用API&#xff0c;总算实现RenderWindowControl函数回调正常串接&#xff0c;当然这个移动处理事件的效果目前也没有确认。 2、使用 vtkImageReslice reslice vtkImageReslice.New();p…...

c++ thread线程join、detach、joinable方法

(621条消息) 线程中断Thread的interrupt()方法_thread interrupt_萝卜阿咕咕的博客-CSDN博客 C/C编程&#xff1a;std::thread 详解-CSDN博客 #include <iostream> #include <thread>void do_some_work() {std::cout<<"Hello Concurrent World\n"…...

Transformer:深度学习的变革力量

深度学习领域的发展日新月异&#xff0c;在自然语言处理&#xff08;NLP&#xff09;、计算机视觉等领域取得了巨大突破。然而&#xff0c;早期的循环神经网络&#xff08;RNN&#xff09;在处理长序列时面临着梯度消失、并行计算能力不足等瓶颈。而 Transformer 的横空出世&am…...

【Python】__main__.py、__init__.py

文章目录 1. __init__.py作用&#xff1a;用法&#xff1a;示例&#xff1a;特点 2. __main__.py作用&#xff1a;用法&#xff1a;示例&#xff1a;特点&#xff1a; 3. 综合示例总结&#xff1a; 1. init.py 作用&#xff1a; __init__.py 文件的主要作用是标识一个目录是一…...

springboot集成整合工作流,activiti审批流,整合实际案例,流程图设计,流程自定义,表单配置自定义,代码demo流程

前言 activiti工作流引擎项目&#xff0c;企业erp、oa、hr、crm等企事业办公系统轻松落地&#xff0c;一套完整并且实际运用在多套项目中的案例&#xff0c;满足日常业务流程审批需求。 一、项目形式 springbootvueactiviti集成了activiti在线编辑器&#xff0c;流行的前后端…...

代码随想录算法【Day16】

Day16 513.找二叉树左下角的值 本题使用迭代法更简单&#xff0c;使用迭代法和递归法的区别是什么 递归法 目标就是找深度最大的叶子结点 无论前中后序遍历&#xff0c;都是左节点先被遍历到&#xff0c;所以一旦得到深度最大的节点&#xff0c;就是最后一行最靠左侧的节点…...

从光子到图像——相机如何捕获世界?

引言 你是否想过为何我们按一下相机快门就可以将眼前广袤多彩的世界显示于一个小小的相机屏幕上&#xff1f;本期推文中将带着大家重现从光子转换为电子、电子转换为图像中数字驱动值的整个流程。 ▲人们通过相机捕获眼前的场景 从光子到电子的转换 光线首先通过光学镜头进入相…...

Harmony开发【笔记1】报错解决(字段名写错了。。)

在利用axios从网络接收请求时&#xff0c;发现返回obj的code为“-1”&#xff0c;非常不解&#xff0c;利用console.log测试&#xff0c;更加不解&#xff0c;可知抛出错误是 “ E 其他错误: userName required”。但是我在测试时&#xff0c;它并没有体现为空&#xff0c;…...

Ubuntu 下载安装 elasticsearch7.17.9

参考 https://blog.csdn.net/qq_26039331/article/details/115024218 https://blog.csdn.net/mengo1234/article/details/104989382 过程 来到 Es 的版本发布列表页面&#xff1a;https://www.elastic.co/downloads/past-releases#elasticsearch 根据自己的系统以及要安装的…...

8. LINUX 用户和组

文章目录 8.1 密码文件&#xff1a;/etc/passwd1. 登录名&#xff08;Login Name&#xff09;2. 经过加密的密码&#xff08;Encrypted Password&#xff09;3. 用户 ID&#xff08;User ID, UID&#xff09;4. 组 ID&#xff08;Group ID, GID&#xff09;5. 注释&#xff08;…...

vue监听中的watch监听(详解)

1、watch 选项用于监听数据的变化并执行相应的回调函数。watch 选项提供了两个重要的属性&#xff1a;deep 和 immediate。1.1、深度监听 (deep: true) 当你需要监听一个对象或数组内部的变化时&#xff0c;可以使用 deep: true。 这会使得 watch 监听器递归地监听对象或数组内…...

微信小程序中 隐藏scroll-view 滚动条 网页中隐藏滚动条

在微信小程序中隐藏scroll-view的滚动条可以通过以下几种方法实现&#xff1a; 方法一&#xff1a;使用CSS隐藏滚动条 在小程序的样式文件中&#xff08;如app.wxss或页面的.wxss文件&#xff09;&#xff0c;添加以下CSS代码来隐藏滚动条&#xff1a; scroll-view ::-webkit…...

K8s Pod OOMKilled,监控却显示内存资源并未打满

1. 问题现象 pod一直重启&#xff0c;通过grafana查看&#xff0c;发现内存使用率并没有100%。 2. 排查过程 2.1 describe查看pod最新一次的状态 可以明显看到&#xff0c;最近一次的重启就是因为内存不足导致的。 2.2 describe 查看node节点状态 找到原因了&#xff0c;原来…...

对话|全年HUD前装将超330万台,疆程技术瞄准人机交互“第一屏”

2024年&#xff0c;在高阶智驾进入快速上车的同时&#xff0c;座舱人机交互也在迎来新的增长点。Chat GPT、AR-HUD、车载投影等新配置都在带来新增量机会。 高工智能汽车研究院监测数据显示&#xff0c;2024年1-10月&#xff0c;中国市场&#xff08;不含进出口&#xff09;乘用…...

【HTML+CSS+JS+VUE】web前端教程-10-列表标签之无序列表

无序列表实现 无序列表是一个项目的列表,此列项目使用粗体圆点(典型的小黑圆圈)进行标记 无序列表始于<ul>标签,每个列表项始于<li>标签。<ul><li>苹果...

基于V2X的无人机与特种车辆战地智能通信:技术融合与实战应用

一、引言 1.1 研究背景与意义 在现代战争的复杂环境中&#xff0c;通信系统的高效与可靠已然成为决定胜负的关键因素。随着军事技术的飞速发展&#xff0c;战争形态发生了深刻变革&#xff0c;作战空间不断拓展&#xff0c;从陆地、海洋、天空延伸至电磁、网络、太空等多维领…...

20250109下载JDK17的方法链接

20250109下载JDK17的方法&链接 2025/1/9 16:20 缘起&#xff1a;编译地面站应用程序QGC&#xff0c;需要安装QT和【旧版本的】JDK17。 当时在网上没有找到JDK17&#xff0c;就安装了比较接近的JDK21。反正最后的QT for Android最后就是没有编译通过。 到底是谁的问题&#…...

杭州铭师堂的云原生升级实践

作者&#xff1a;升学e网通研发部基建团队 公司介绍 杭州铭师堂&#xff0c;是一个致力于为人的全面发展而服务的在线教育品牌。杭州铭师堂秉持“用互联网改变教育&#xff0c;让中国人都有好书读”的使命&#xff0c;致力于用“互联网教育”的科技手段让更多的孩子都能享有优…...

chrome浏览器的更新提示弹窗无法更新Chrome解决方法

使用组策略编辑器 此方法适用于 Windows 系统且系统为专业版及以上版本&#xff0c;家庭版系统没有组策略功能。 按下Win R键&#xff0c;打开 “运行” 对话框&#xff0c;输入gpedit.msc并回车&#xff0c;打开组策略编辑器。 在组策略编辑器中&#xff0c;依次展开 “计算机…...

LLM prompt提示构造案例:语音回复内容;o1思维链

1、语音回复内容 目的&#xff1a; 语音聊天助手的prompt&#xff0c;让大模型来引导聊天内容&#xff0c;简短和友好&#xff0c;从而文字转语音时候也比较高效。 ## 角色设定与交互规则 ### 基本角色 你是用户的好朋友. 你的回答将通过逼真的文字转语音技术阅读. ### 回答规则…...

OceanBase 学习计划全攻略:开启分布式数据库探索之旅

《OceanBase 学习计划全攻略&#xff1a;开启分布式数据库探索之旅》 在当今数字化浪潮汹涌澎湃的时代&#xff0c;数据库作为企业信息存储与管理的核心基础设施&#xff0c;其性能、可靠性和扩展性至关重要。OceanBase 作为一款具有卓越分布式特性的国产数据库&#xff0c;正…...

Linux 虚拟机与windows主机之间的文件传输--设置共享文件夹方式

Linux 虚拟机与windows主机之间的文件传输 设置共享文件夹方式 在虚拟机中打开终端查看是否已经新建完成&#xff0c;到文件夹中找到它看一下&#xff0c;这个位置就能存储东西啦...

React Context用法总结

1. 基本概念 1.1 什么是 Context Context 提供了一种在组件树中共享数据的方式&#xff0c;而不必通过 props 显式地逐层传递。它主要用于共享那些对于组件树中许多组件来说是"全局"的数据。 1.2 基本用法 // 1. 创建 Context const ThemeContext React.createC…...

Linux好用软件

力荐软件 apt-fast:更快速的软件管理安装过程会进入一个图形界面,配置线程数等信息,全部默认即可 sudo add-apt-repository ppa:apt-fast/stable sudo apt-get update sudo apt-get -y install apt-fast 以后安装应用,把apt-get直接替换成apt-fast即可,例如安装vlc sudo…...

【MYSQL】

文章目录 1.DDL 1.DDL --添加字段 ALTER TABLE table_name add COLUMN embed_model VARCHAR(32) NOT NULL COMMENT 名称备注 COLLATE utf8mb4_bin AFTER config_code;--修改字段 ALTER TABLE table_name CHANGE COLUMN column_a column_b VARCHAR(500) NOT NULL COMMENT 配置信…...

webrtc之rtc::ArrayView<const uint8_t>

rtc::ArrayView<const uint8_t> 是 WebRTC&#xff08;或其他基于 rtc 命名空间的库&#xff09;中常见的一个类型&#xff0c;它通常用于表示一块 只读的内存区域&#xff0c;该内存区域由一系列 uint8_t 类型&#xff08;无符号 8 位整数&#xff09;元素组成。 1. rt…...

深入理解 MySQL 的 EXPLAIN 工具

1. 什么是 EXPLAIN 工具&#xff1f; EXPLAIN 是 MySQL 中用来分析 SQL 查询执行计划的命令&#xff0c;它能够显示查询在执行时会如何访问表、使用哪些索引、扫描多少行等信息。通过 EXPLAIN 工具&#xff0c;开发者可以直观地了解查询的执行过程&#xff0c;从而进行针对性的…...

谷歌Google、紫鸟浏览器插件开发

对于跨境电商行业的IT部门来说&#xff0c;经常需要获取各种店铺相关数据&#xff0c;但是仅靠官方提供的接口来获取数据远远不够&#xff0c;这个时候我们就需要插件或者RPA的方式来获取数据。 以下是关于自研紫鸟插件的简单demo&#xff0c;紫鸟浏览器使用的是火狐和谷歌的插…...