Java 并发编程知识总结【三】

4. CompletableFuture

4.1 Future 和 Callable 接口

Future 接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。

image-20221231114844131

Callable 接口中定义了需要有返回的任务需要实现的方法。

image-20221231114936534

使用途径:比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会才去获取子任务的执行结果。

4.2 FutureTask

是什么:未来的任务,用它就干一件事,异步调用main方法。就像一个冰糖葫芦,一个个方法由main串起来。但解决不了一个问题:正常调用挂起堵塞问题

例子:

(1)老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙买水,水买回来了放桌上,我需要的时候再去get。

(2)4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,是不是C的计算量有点大啊,FutureTask单起个线程给C计算,我先汇总ABD,最后等C计算完了再汇总C,拿到最终结果

(3)高考:会做的先做,不会的放在后面做

原理:在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成。当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态( get 方法)。一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。 只计算一次,get 方法一般放到最后

FutureTask 类的关系图

image-20221231115316676

get() 阻塞

public static void main(String[] args) throws Exception {
    FutureTask<Integer> futureTask = new FutureTask<>(() -> {
        System.out.println("-----come in FutureTask");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return ThreadLocalRandom.current().nextInt(100);
    });
    Thread t1 = new Thread(futureTask, "t1");
    t1.start();
    // 3秒钟后才出来结果,还没有计算你提前来拿(只要一调用get方法,对于结果就是不见不散,会导致阻塞)
    //        System.out.println(Thread.currentThread().getName() + "\t" + futureTask.get());

    // 3秒钟后才出来结果,我只想等待1秒钟,过时不候     然后报 TimeoutException
    //        System.out.println(Thread.currentThread().getName() + "\t" + futureTask.get(1, TimeUnit.SECONDS));

    System.out.println(Thread.currentThread().getName() + "\t" + " run... here");
}

一旦调用get()方法,不管是否计算完成都会导致阻塞

isDone() 轮询

public static void main(String[] args) throws Exception {
    FutureTask<String> futureTask = new FutureTask<>(() -> {
        System.out.println("-----come in FutureTask");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "" + ThreadLocalRandom.current().nextInt(100);
    });
    new Thread(futureTask, "t1").start();

    // 用于阻塞式获取结果,如果想要异步获取结果,通常都会以轮询的方式去获取结果
    while (true) {
        if (futureTask.isDone()) {
            System.out.println("计算完毕,结果为:" + futureTask.get());
            break;
        } else {
            System.out.println("还在计算中");
        }
    }
    System.out.println(Thread.currentThread().getName() + "\t" + " run... here");
}

轮询的方式会耗费无谓的 CPU 资源,而且也不见得能及时地得到计算结果。

如果想要异步获取结果,通常都会以轮询的方式去获取结果尽量不要阻塞。

但是我们想要完成一些复杂的任务,如下:

  • 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
  • 将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果
  • 当Future集合中某个任务最快结束时,返回结果
  • 等待Future结合中的所有任务都完成

对于上述的任务,当我们继续使用 futureTask 时就会显得很累赘,而且还会阻塞,这时候我们就要考虑采用新的技术。

4.3 对Future的改进

4.3.1 CompletableFuture 和 CompletionStage 介绍

类架构说明

image-20221231120649961

接口 CompletionStage

image-20221231120811352

代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。

CompletableFuture

image-20221231120851065

4.3.2 核心的四个静态方法,来创建一个异步操作

runAsync 无返回值

  • public static CompletableFuture<Void> runAsync(Runnable runnable)
  • public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)

supplyAsync 有返回值

  • public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
  • public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

Executor executor 参数说明:

  • 没有指定 Executor 的方法,直接使用默认的ForkJoinPool.commonPool() 作为它的线程池执行异步代码
  • 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码

代码:

public static void main(String[] args) throws Exception {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 2, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
        System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
        System.out.println("-----task is over");
    });
    System.out.println(future1.get());
    CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
        System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
        System.out.println("-----task is over");
    }, executor);
    System.out.println(future2.get());
    CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
        System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
        System.out.println("-----task is over");
        return 11;
    });
    System.out.println(future3.get());
    CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(() -> {
        System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
        System.out.println("-----task is over");
        return 11;
    }, executor);
    System.out.println(future4.get());
    executor.shutdown();
}
// 结果------使用 get 方法仍然会阻塞,因为仍然可以使用 Future 接口中的方法
ForkJoinPool.commonPool-worker-1	-----come in
-----task is over
null
pool-1-thread-1	-----come in
-----task is over
null
ForkJoinPool.commonPool-worker-1	-----come in
-----task is over
11
pool-1-thread-1	-----come in
-----task is over
11

常用 Code 演示,减少阻塞和轮询:

从 Java8 开始引入了 CompletableFuture,它是 Future 的功能增强版,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法

public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
        int result = ThreadLocalRandom.current().nextInt(10);
        // 暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----计算结束耗时1秒钟,result: " + result);
        if (result > 6) {
            int age = 10 / 0;
        }
        return result;
    }, executor).thenApply(f -> {
        System.out.println("--------------继续计算-----------,进行 + 2 操作");
        return f + 2;
    }).whenComplete((r, e) -> {
        if (e == null) {
            System.out.println("-----result: " + r);
        }
    }).exceptionally(e -> {
        System.out.println("-----exception: " + e.getCause() + "\t" + e.getMessage());
        return -1;
    });
    // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(future.join());
    System.out.println("----------------main over");
    executor.shutdown();
}
// 结果之一:
pool-1-thread-1	-----come in
-----计算结束耗时1秒钟,result: 3
--------------继续计算-----------,进行 + 2 操作
-----result: 5
5
----------------main over
// 结果之二:
pool-1-thread-1	-----come in
-----计算结束耗时1秒钟,result: 8
-----exception: java.lang.ArithmeticException: / by zero	java.lang.ArithmeticException: / by zero
-1
----------------main over  

CompletableFuture 的优点:

  • 异步任务结束时,会自动回调某个对象的方法
  • 异步任务出错时,会自动回调某个对象的方法
  • 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行

4.4 案例精讲-从电商网站的比价需求

4.4.1 回顾函数式编程

image-20221231140102602

Lambda +Stream+链式调用+Java8函数式编程带走

说下 joinget 的区别?

joinget 没有区别,唯一不同点就是,join 方法无需抛出异常,get 方法需要手动抛出异常;而且二者都会阻塞。

4.4.2 业务需求

案例说明:电商比价需求

  • 同一款产品,同时搜索出同款产品在各大电商的售价;
  • 同一款产品,同时搜索出本产品在某一电商平台下,各个入驻门店的售价是多少

出来结果希望是同款产品在不同价格清单列表,返回一个List

《MySQL》 in jd price is 88.05

《MySQL》 in pdd price is 86.01

《MySQL》 in tabobao price is 90.43

要求深刻理解:

  • 函数式编程
  • 链式编程
  • Stream 流式计算

方案:

经常出现在等待某条 SQL 执行完成后,再继续执行下一条 SQL ,而这两条 SQL 本身是并无关系的,可以同时进行执行的。

我们希望能够两条 SQL 同时进行处理,而不是等待其中的某一条 SQL 完成后,再继续下一条。同理,对于分布式微服务的调用,按照实际业务,如果是无关联 step by step 的业务,可以尝试是否可以多箭齐发,同时调用。

  1. step by step,查完京东查淘宝,查完淘宝查天猫…
  2. all 一口气同时查询。。。。。

切记,功能→性能

代码如下:

public class CompletableFutureNetMallDemo {

    static List<NetMall> list = Arrays.asList(
            new NetMall("jd"),
            new NetMall("tmall"),
            new NetMall("pdd"),
            new NetMall("mi")
    );

    public static List<String> findPriceSync(List<NetMall> list, String productName) {
        return list.stream()
                .map(netMall -> String.format(productName + " %s price is %.2f", netMall.getNetMallName(), netMall.getPriceByName(productName)))
                .collect(Collectors.toList());
    }

    public static List<String> findPriceASync(List<NetMall> list, String productName) {
        return list.stream()
                .map(netMall -> CompletableFuture.supplyAsync(() -> String.format(productName + " %s price is %.2f", netMall.getNetMallName(), netMall.getPriceByName(productName))))
                .toList()
                .stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        List<String> list1 = findPriceSync(list, "thinking in java");
        for (String element : list1) {
            System.out.println(element);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("----costTime: " + (endTime - startTime) + " 毫秒");

        long startTime2 = System.currentTimeMillis();
        List<String> list2 = findPriceASync(list, "thinking in java");
        for (String element : list2) {
            System.out.println(element);
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println("----costTime: " + (endTime2 - startTime2) + " 毫秒");
    }
}

class NetMall {
    @Getter
    private String netMallName;

    public NetMall(String netMallName) {
        this.netMallName = netMallName;
    }

    public double getPriceByName(String productName) {
        return calcPrice(productName);
    }

    private double calcPrice(String productName) {
        // 检索 消耗 1s
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return ThreadLocalRandom.current().nextDouble() + productName.charAt(0);
    }
}
// 运行结果:
thinking in java jd price is 116.93
thinking in java tmall price is 116.13
thinking in java pdd price is 116.03
thinking in java mi price is 116.69
----costTime: 4053 毫秒
thinking in java jd price is 116.39
thinking in java tmall price is 116.40
thinking in java pdd price is 116.63
thinking in java mi price is 116.33
----costTime: 1008 毫秒
// 并且当我们增加查询的商铺的数量时,方案1的耗时会继续增加,但是方案2仍然大约为1秒

4.5 CompletableFuture常用方法

4.5.1 获得结果和触发计算

获取结果

  • public T get() 不见不散
  • public T get(long timeout, TimeUnit unit) 过时不候
  • public T getNow(T valueIfAbsent) 没有计算完成的情况下,给我一个替代结果,立即获取结果,不阻塞;计算完,返回计算完成后的结果,没算完,返回设定的 valueIfAbsent 值
public static void main(String[] args) {
    CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 533;
    });
    // 去掉注释上面计算没有完成,返回444
    // 开启注释上满计算完成,返回计算结果
    //        try {
    //            TimeUnit.SECONDS.sleep(2);
    //        } catch (InterruptedException e) {
    //            e.printStackTrace();
    //        }
    System.out.println(completableFuture.getNow(444));
}
  • public T join()

主动触发计算

  • public boolean complete(T value) 是否打断 get 方法立即返回括号值
public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 533;
    });
    //        try {
    //            TimeUnit.SECONDS.sleep(2);
    //        } catch (InterruptedException e) {
    //            e.printStackTrace();
    //        }
    // 当调用 CompletableFuture.get() 被阻塞的时候, complete 方法就是结束阻塞并 get() 获取设置的 complete 里面的值.
    System.out.println(completableFuture.complete(444) + "\t" + completableFuture.get());
}
// 结果
true	444
4.5.2 对计算结果进行处理

thenApply

计算结果存在依赖关系,这两个线程串行化。

由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    // 当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化
    CompletableFuture.supplyAsync(() -> {
        SmallTool.sleepMillis(1000);
        SmallTool.printTimeAndThread("111");
        return 1;
    }, executor).thenApply(f -> {
        SmallTool.printTimeAndThread("222");
        // int i = 10 / 0; // 异常情况:那步出错就停在那步。
        return f + 2;
    }).thenApply(f -> {
        SmallTool.printTimeAndThread("333");
        return f + 3;
    }).thenApply(f -> {
        SmallTool.printTimeAndThread("444");
        return f + 4;
    }).whenComplete((r, e) -> {
        if (e == null) {
            SmallTool.printTimeAndThread("result:" + r);
        }
    }).exceptionally(e -> {
        SmallTool.printTimeAndThread(e.getMessage());
        return null;
    });
    SmallTool.printTimeAndThread("-----主线程结束,END");
    executor.shutdown();
}
// 结果
1672472051258	|	1	|	main	|	-----主线程结束,END
1672472052262	|	24	|	pool-1-thread-1	|	111
1672472052263	|	24	|	pool-1-thread-1	|	222
1672472052263	|	24	|	pool-1-thread-1	|	java.lang.ArithmeticException: / by zero

handle

有异常也可以往下一步走,根据带的异常参数可以进一步处理

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
  
    CompletableFuture.supplyAsync(() -> {
        SmallTool.sleepMillis(1000);
        SmallTool.printTimeAndThread("111");
        return 1;
    }, executor).handle((f, e) -> {
        SmallTool.printTimeAndThread("222");
        int i = 10 / 0; 
        return f + 2;
    }).handle((f, e) -> {
        SmallTool.printTimeAndThread("333");
        return f + 3;
    }).handle((f, e) -> {
        SmallTool.printTimeAndThread("444");
        return f + 4;
    }).whenComplete((r, e) -> {
        if (e == null) {
            SmallTool.printTimeAndThread("result:" + r);
        }
    }).exceptionally(e -> {
        SmallTool.printTimeAndThread(e.getMessage());
        return null;
    });
    SmallTool.printTimeAndThread("-----主线程结束,END");
    executor.shutdown();
}
// 结果
1672472183766	|	1	|	main	|	-----主线程结束,END
1672472184777	|	24	|	pool-1-thread-1	|	111
1672472184777	|	24	|	pool-1-thread-1	|	222
1672472184777	|	24	|	pool-1-thread-1	|	333
1672472184782	|	24	|	pool-1-thread-1	|	444
1672472184782	|	24	|	pool-1-thread-1	|	java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "f" is null

总结:

image-20221231153753654

image-20221231153833197

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    // 当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化
    CompletableFuture.supplyAsync(() -> {
        SmallTool.sleepMillis(1000);
        SmallTool.printTimeAndThread("111");
        return 1;
    }, executor).thenApply(f -> {
        SmallTool.printTimeAndThread("222");
        //            int i = 10 / 0; // 异常情况:那步出错就停在那步。
        return f + 2;
    }).thenApply(f -> {
        SmallTool.printTimeAndThread("333");
        return f + 3;
    }).thenApply(f -> {
        SmallTool.printTimeAndThread("444");
        return f + 4;
    }).whenCompleteAsync((r, e) -> {	// 使用 whenCompleteAsync 使用默认线程池
        if (e == null) {
            SmallTool.printTimeAndThread("result:" + r);
        }
    }).exceptionally(e -> {
        SmallTool.printTimeAndThread(e.getMessage());
        return null;
    });
    SmallTool.printTimeAndThread("-----主线程结束,END");
    SmallTool.sleepMillis(2000);
    executor.shutdown();
}
// 结果
1672472472269	|	1	|	main	|	-----主线程结束,END
1672472473269	|	24	|	pool-1-thread-1	|	111
1672472473270	|	24	|	pool-1-thread-1	|	222
1672472473270	|	24	|	pool-1-thread-1	|	333
1672472473270	|	24	|	pool-1-thread-1	|	444
1672472473272	|	25	|	ForkJoinPool.commonPool-worker-1	|	result:10
4.5.3 对计算结果进行消费

接收任务的处理结果,并消费处理,无返回结果

thenAccept

CompletableFuture.supplyAsync(() -> {
    int result = 0;
    for (int i = 1; i <= 10; i++) {
        result += i;
    }
    return result;
}).thenApply(number -> {
    for (int i = 11; i <= 20; i++) {
        number += i;
    }
    return number;
}).thenApply(number -> {
    for (int i = 21; i <= 30; i++) {
        number += i;
    }
    return number;
}).thenAccept(result -> SmallTool.printTimeAndThread("result:" + result));
// 结果
1672474582425	|	1	|	main	|	result:465

任务之间的顺序执行

  • thenRun(Runnable runnable):任务 A 执行完执行 B,并且 B 不需要 A 的结果

  • thenAccept(Consumer action):任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 无返回值

  • thenApply(Function fn):任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值

4.5.4 对计算速度进行选用

谁快用谁

applyToEither

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture.supplyAsync(() -> {
        SmallTool.printTimeAndThread("1号车在来的路上");
        SmallTool.sleepMillis(1000);
        return "1号车";
    }).applyToEither(CompletableFuture.supplyAsync(() -> {
        SmallTool.printTimeAndThread("2号车在来的路上");
        SmallTool.sleepMillis(2000);
        return "2号车";
    }), r -> {
        SmallTool.printTimeAndThread(r + "先来了");
        return r;
    }).join();
}
// 结果
1672474977959	|	25	|	ForkJoinPool.commonPool-worker-2	|	2号车在来的路上
1672474977959	|	24	|	ForkJoinPool.commonPool-worker-1	|	1号车在来的路上
1672474978966	|	24	|	ForkJoinPool.commonPool-worker-1	|	1号车先来了
4.5.5 对计算结果进行合并

两个 CompletionStage 任务都完成后,最终能把两个任务的结果一起交给 thenCombine 来处理

先完成的先等着,等待其它分支任务

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
CompletableFuture.supplyAsync(() -> {
    SmallTool.printTimeAndThread("---come in 1");
    SmallTool.sleepMillis(1000);
    return 10;
}, executor).thenCombine(CompletableFuture.supplyAsync(() -> {
    SmallTool.printTimeAndThread("---come in 2");
    SmallTool.sleepMillis(2000);
    return 20;
}), (x, y) -> {
    SmallTool.printTimeAndThread("---come in 3");
    return x + y;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
    SmallTool.printTimeAndThread("---come in 4");
    return 30;
}), (x, y) -> {
    SmallTool.printTimeAndThread("---come in 5");
    return x + y;
}).thenAccept(r -> SmallTool.printTimeAndThread("result:" + r));
// executor.shutdown();
// 结果
1672475423414	|	24	|	pool-1-thread-1	|	---come in 1
1672475423415	|	25	|	ForkJoinPool.commonPool-worker-1	|	---come in 2
1672475423416	|	26	|	ForkJoinPool.commonPool-worker-2	|	---come in 4
1672475425420	|	25	|	ForkJoinPool.commonPool-worker-1	|	---come in 3
1672475425420	|	25	|	ForkJoinPool.commonPool-worker-1	|	---come in 5
1672475425421	|	25	|	ForkJoinPool.commonPool-worker-1	|	result:60
4.5.6 对计算结果进行连接

thenCompose

连接两个有依赖关系的任务 结果由第二个任务返回

public static void main(String[] args) throws ExecutionException, InterruptedException {
    SmallTool.printTimeAndThread("小白进入餐厅");
    SmallTool.printTimeAndThread("小白点了 番茄炒蛋 + 一碗米饭");

    // thenCompose 连接两个有依赖关系的任务 结果由第二个任务返回
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        SmallTool.printTimeAndThread("厨师炒菜");
        SmallTool.sleepMillis(200);
        return "番茄炒蛋";
    }).thenCompose(dish -> CompletableFuture.supplyAsync(() -> {
        SmallTool.printTimeAndThread("服务员打饭");
        SmallTool.sleepMillis(100);
        return dish + " 米饭";
    }));

    SmallTool.printTimeAndThread("小白在打王者");
    SmallTool.printTimeAndThread(cf1.join() + ",小白开吃");
}
// 结果
1672475923655	|	1	|	main	|	小白进入餐厅
1672475923655	|	1	|	main	|	小白点了 番茄炒蛋 + 一碗米饭
1672475923658	|	24	|	ForkJoinPool.commonPool-worker-1	|	厨师炒菜
1672475923658	|	1	|	main	|	小白在打王者
1672475923875	|	25	|	ForkJoinPool.commonPool-worker-2	|	服务员打饭
1672475923983	|	1	|	main	|	番茄炒蛋 米饭,小白开吃

更多文章在我的语雀平台:https://www.yuque.com/ambition-bcpii/muziteng

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ambition0823

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值