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

news2024/11/16 20:38:32

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/132382.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

详解Spring面试AOP

文章目录什么是 AOP&#xff1f;AOP作用AOP核心概念&#xff08;来自黑马程序课程&#xff09;AOP 解决了什么问题&#xff1f;AOP 为什么叫做切面编程&#xff1f;总结1 AOP的核心概念2 切入点表达式3 五种通知类型4 通知中获取参数AOP是面向切面编程&#xff0c;是一个设计思…

Java同学录系统同学录网站

简介 用户注册可以创建班级&#xff08;创建者即为群主&#xff09;&#xff0c;用户也可以查找班级申请加入&#xff0c;群主添加同学的联系方式等&#xff0c;可以在班级里留言&#xff0c;管理相册等&#xff0c;还可以指定其他人为群主或者解散班级群&#xff0c;群里的用…

【ROS】—— ROS快速上手(一)

文章目录前言1. ROS-melodic 安装2. ROS基本操作2.1 创建工作空间2.2 创建功能包2.3 HelloWorld(C版)2.4 HelloWorld(Python版)3. Vscode ROS 插件4. vscode 使用基本配置4.1 启动 vscode4.2 vscode 中编译 ros5. launch文件演示6. ROS文件系统7. ROS文件系统相关命令前言 &…

EMNLP22评测矩阵:FineD-Eval: Fine-grained Automatic Dialogue-Level Evaluation

总结 在选择维度时&#xff0c;有点意思。 FineD-Eval: Fine-grained Automatic Dialogue-Level Evaluation 一般对话生成任务的评测也是从多个维度出发&#xff0c;这篇文章先选择了几个相关性程度低的维度&#xff0c;然后&#xff0c;在挑选后的维度上&#xff0c;测评相…

动态规划经典题:编辑距离(hard) 详解,看了还不会你来砍我

&#x1f9f8;&#x1f9f8;&#x1f9f8;各位大佬大家好&#xff0c;我是猪皮兄弟&#x1f9f8;&#x1f9f8;&#x1f9f8; 文章目录一、最长公共子序列二、两个字符串的删除操作三、编辑距离Hard为了更好的理解&#xff0c;我们从易到难的来解决编辑距离的问题一、最长公共…

Tic-Tac-Toe可能棋局遍历的实现(python)

目录 1. 前言 2. 算法流程 3. 代码实现 4. 一个思考题&#xff1a;代码实现中的一个坑 5. 结果正确吗&#xff1f; 1. 前言 在上一篇博客中&#xff1a;Tic-Tac-Toe可能棋局搜索的实现&#xff08;python&#xff09;_笨牛慢耕的博客-CSDN博客Tic-Tac-Toe中文常译作井字棋…

基础数学(五)——数值积分

文章目录考试要求基础概念代数精度&#xff08;必考题&#xff09;代数精度的定义求代数精度的例题&#xff08;期末考试数值积分第一个大题&#xff09;数值积分公式的构造插值型求积公式&#xff08;必考题&#xff09;插值型数值积分公式定理Newton-Cotes求积公式Cotes公式代…

webpack 学习

1.拆分、合并 webpack-merge devlopment production 2.webpack-dev-serve devServer: { port contentBase progress open compress proxy:{ xxx:{ target:..., pathRewrite:{ "^/api" }, changeOrigin:true } } } 3.处理样式 css module:{ rules:[ { test:/\.sc…

qt开关控件设计(手把手从零开始)

从零开始手把手教你设计自己的qt控件1 说明1.1 显示效果1.2 控件特性1.3 设计方法2 控件需求分析2.1 必要需求2.1 顺带需求&#xff08;锦上添花&#xff09;3 功能设计3.1 设计思路&#xff08;重点内容&#xff09;3.2 自适应大小3.3 开关动画3.4 控件绘制4 总体代码1 说明 …

推动新能源越野场景革命 坦克品牌开创越野新生态

近日&#xff0c;坦克品牌以“创领越野新生态”为主题&#xff0c;携多款车型登陆第二十届广州国际汽车展览会。秉持“以用户为中心”&#xff0c;坦克品牌围绕技术与生态双线出击&#xff0c;正式亮相坦克500 PHEV长续航版、生活方式共创平台TANK Life。技术创领&#xff0c;打…

docker-ui创建使用

首先需要安装docker: apt install docker.io composer也需要安装&#xff1a; apt install composer docker查找docker-ui镜像&#xff1a; docker search docker-ui 在列表里选一个镜这里就选第一个。 docker pull builtdock/docker-ui 然后直接docker run 使用的时候&#x…

DSL操作ElasticSearch基础命令

文章目录一、DSL操作ES-RESTful风格二、DSL操作索引库2.1 PUT 添加索引2.2 GET 查询索引2.3 DELETE 删除索引2.4 POST 打开/关闭索引库三、DSL操作映射3.1 数据类型3.1.1 简单数据类型3.1.2 复杂数据类型3.2 创建索引库并设置映射3.2.1 语法格式3.2.2 举例3.3 查询索引库映射3.…

python中的类型注解

目录 一.类型注解 变量的类型注解 类型注解的语法 类型注解主要功能在于: 函数方法的类型注解 函数&#xff08;方法&#xff09;形参进行类型注解 函数&#xff08;方法&#xff09;返回值进行类型注解 小结 Union类型 小结 一.类型注解 变量的类型注解 思考 为什么…

Android美团多渠道打包Walle集成

一、为什么使用美团多渠道打包的方式&#xff1f; 打包更加快速 传统的通过productFlavors渠道包的方式&#xff0c;渠道10个以内还可以接受&#xff0c;如果100个渠道包&#xff0c;每个包需要打5Min,就是将近10个小时的打包&#xff0c;而采用美团Walle多渠道打包的方式只需…

PyTorch 2.0 推理速度测试:与 TensorRT 、ONNX Runtime 进行对比

PyTorch 2.0 于 2022 年 12 月上旬在 NeurIPS 2022 上发布&#xff0c;它新增的 torch.compile 组件引起了广泛关注&#xff0c;因为该组件声称比 PyTorch 的先前版本带来更大的计算速度提升。 这对我们来说是一个好消息&#xff0c;训练时间改进的结果令人印象深刻。PyTorch 团…

JavaScript 入门基础 - 流程控制(四)

JavaScript 流程控制 - 分支和循环 文章目录JavaScript 流程控制 - 分支和循环1. 什么是流程控制2. 顺序流程控制3. 分支流程控制 之 if语句3.1 什么是分支结构3.2 if 语句3.2.1 if 语句基本理解3.2.2 if 语句执行流程3.2.3 if 语句案例3.3 if else语句&#xff08;双分支语句&…

Threejs实现鼠标点击人物行走/镜头跟随人物移动/鼠标点击动画/游戏第三人称/行走动作

1&#xff0c;功能介绍 Threejs获取鼠标点击位置、实现鼠标点击人物行走、人物头顶显示名称标签、镜头跟随人物移动并且镜头围绕人物旋转&#xff0c;类似游戏中第三人称、鼠标点击位置有动画效果&#xff0c;如下效果图 2&#xff0c;功能实现 获取鼠标点击位置&#xff0c;…

【Linux】进程间通信 - 匿名/命名管道与System V共享内存

目录 前言 一.管道 0.什么是管道 1).管道的概念 2).管道的本质 3).管道指令: "|" 1.匿名管道 1).如何创建匿名管道 2).如何使用匿名管道进行通信 3).匿名管道的特点总结 2.命名管道 0).指令级的命名管道的通信 1).如何在编程时创建命名管道 2).如何在…

你好2023-使用msys64 openssl 制作QSslSocket实验所需证书

2023年开始了&#xff0c;第一篇&#xff0c;记录最近帮朋友制作QSslSocket所需证书的过程。 使用传统的TCP连接依旧是很多工业软件的常见通信方法。但如果恰好不希望别人通过抓包等方法研究上位机和控制器模块之间的协议格式&#xff0c;那使用SSL连接是一种掩耳盗铃的好办法&…

Pyinstaller - 你的“神”队友

哈哈&#xff01;今天是我在2023年发布的第一篇文章呀&#xff01; 这两天&#xff0c;我在做一个爬虫项目。因为我做好后准备给我的朋友看看&#xff0c;但我朋友没有 Python 环境。所以&#xff0c;只好想办法把 .py 打包成 .exe 。 在网上搜了一下&#xff0c;发现目前相对…