场景

场景 方法
任务少, 不频繁 直接使用线程
任务数稳定,频繁 使用线程池

线程池

优点

  • 不用频繁的创建和销毁线程
  • 不需要担心OOM
  • 直接往任务队列添加任务即可
  • 核心线程忙不过来,可以自动增加到最大线程数

    构造参数

  • 核心线程数

  • 最大线程数
  • 空闲活跃时长
  • 时长单位
  • 阻塞队列
  • 线程工厂
  • 拒绝策略
    • 直接丢弃
    • 替换最后一个
    • 抛异常
    • 谁提交的任务谁执行
    • —- 自行 扩展

      工具类

      Executors
方法 描述
newSingleThreadExecutor 创建一个单线程的线程池
newCachedThreadPool 创建一个无上限的线程池(Integer.MAX)
newFixedThreadPool 创建一个固定线程数的线程池

需求

小白和他的朋友门,连续输了10几把游戏, 决定去餐厅吃饭了,3个人,直接点了10盘菜,决定化悲愤为食量

实现

编写代码

先将之前的公共方法抽成一个工具类

  1. package com.dance;
  2. import java.util.StringJoiner;
  3. public class SmallTool {
  4. /**
  5. * 休眠方法
  6. * @param millis 毫秒
  7. */
  8. public static void sleep(long millis){
  9. try {
  10. Thread.sleep(millis);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. /**
  16. * 打印方法
  17. * @param text 文本
  18. */
  19. public static void print(String text){
  20. String str = new StringJoiner("\t|\t")
  21. .add(String.valueOf(System.currentTimeMillis()))
  22. .add(String.valueOf(Thread.currentThread().getId()))
  23. .add(Thread.currentThread().getName())
  24. .add(text)
  25. .toString();
  26. System.out.println(str);
  27. }
  28. }

创建菜类

  1. package com.dance;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * 菜
  5. */
  6. public class Dish {
  7. /**
  8. * 菜名
  9. */
  10. private final String name;
  11. /**
  12. * 用时(秒)
  13. */
  14. private final Integer productionTime;
  15. public Dish(String name, Integer productionTime) {
  16. this.name = name;
  17. this.productionTime = productionTime;
  18. }
  19. /**
  20. * 做菜
  21. */
  22. public void make(){
  23. SmallTool.sleep(TimeUnit.SECONDS.toMillis(productionTime));
  24. SmallTool.print(name + "制作完毕 来吃我吧!");
  25. }
  26. }

编写过程

  1. @Test
  2. public void testOne(){
  3. SmallTool.print("小白和小伙伴门 进餐厅点菜");
  4. long startTime = System.currentTimeMillis();
  5. ArrayList<Dish> dishes = new ArrayList<>();
  6. // 点菜
  7. for (int i = 1; i <= 10; i++) {
  8. dishes.add(new Dish("菜" + i, 1));
  9. }
  10. // 做菜
  11. dishes.forEach(dish -> CompletableFuture.runAsync(dish::make).join());
  12. SmallTool.print("菜都做好了, 上桌 " + (System.currentTimeMillis() - startTime));
  13. }

执行结果

  1. 1649519841265 | 1 | main | 小白和小伙伴门 进餐厅点菜
  2. 1649519842281 | 24 | ForkJoinPool.commonPool-worker-19 | 1制作完毕 来吃我吧!
  3. 1649519843286 | 24 | ForkJoinPool.commonPool-worker-19 | 2制作完毕 来吃我吧!
  4. 1649519844294 | 24 | ForkJoinPool.commonPool-worker-19 | 3制作完毕 来吃我吧!
  5. 1649519845300 | 24 | ForkJoinPool.commonPool-worker-19 | 4制作完毕 来吃我吧!
  6. 1649519846307 | 24 | ForkJoinPool.commonPool-worker-19 | 5制作完毕 来吃我吧!
  7. 1649519847313 | 24 | ForkJoinPool.commonPool-worker-19 | 6制作完毕 来吃我吧!
  8. 1649519848319 | 24 | ForkJoinPool.commonPool-worker-19 | 7制作完毕 来吃我吧!
  9. 1649519849326 | 24 | ForkJoinPool.commonPool-worker-19 | 8制作完毕 来吃我吧!
  10. 1649519850333 | 24 | ForkJoinPool.commonPool-worker-19 | 9制作完毕 来吃我吧!
  11. 1649519851339 | 24 | ForkJoinPool.commonPool-worker-19 | 10制作完毕 来吃我吧!
  12. 1649519851343 | 1 | main | 菜都做好了, 上桌 10075

好像没什么问题, 但是这样的话, 一个一个调用join,硬是把多线程玩成了单线程~

代码改造

@Test
public void testTwo(){

    SmallTool.print("小白和小伙伴门 进餐厅点菜");

    long startTime = System.currentTimeMillis();

    ArrayList<Dish> dishes = new ArrayList<>();

    // 点菜
    for (int i = 1; i <= 10; i++) {
        dishes.add(new Dish("菜" + i, 1));
    }

    ArrayList<CompletableFuture<Void>> completableFutures = new ArrayList<>();

    // 做菜 将所有线程引用收集
    dishes.forEach(dish -> completableFutures.add(CompletableFuture.runAsync(dish::make)));

    // 将所有线程统一join
    CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join();

    SmallTool.print("菜都做好了, 上桌 " + (System.currentTimeMillis() - startTime));

}

执行结果

1649520172283    |    1    |    main    |    小白和小伙伴门 进餐厅点菜
1649520173305    |    30    |    ForkJoinPool.commonPool-worker-31    |    菜7制作完毕 来吃我吧!
1649520173305    |    27    |    ForkJoinPool.commonPool-worker-23    |    菜3制作完毕 来吃我吧!
1649520173305    |    32    |    ForkJoinPool.commonPool-worker-3    |    菜9制作完毕 来吃我吧!
1649520173305    |    28    |    ForkJoinPool.commonPool-worker-27    |    菜5制作完毕 来吃我吧!
1649520173305    |    26    |    ForkJoinPool.commonPool-worker-9    |    菜4制作完毕 来吃我吧!
1649520173305    |    24    |    ForkJoinPool.commonPool-worker-19    |    菜2制作完毕 来吃我吧!
1649520173305    |    25    |    ForkJoinPool.commonPool-worker-5    |    菜1制作完毕 来吃我吧!
1649520173305    |    29    |    ForkJoinPool.commonPool-worker-13    |    菜6制作完毕 来吃我吧!
1649520173305    |    33    |    ForkJoinPool.commonPool-worker-21    |    菜10制作完毕 来吃我吧!
1649520173305    |    31    |    ForkJoinPool.commonPool-worker-17    |    菜8制作完毕 来吃我吧!
1649520173335    |    1    |    main    |    菜都做好了, 上桌 1049

哇咔咔, 不得了呀, 原本10秒的事情, 居然只用了一秒

使用Stream优化代码

@Test
public void testTwo(){

    SmallTool.print("小白和小伙伴门 进餐厅点菜");

    long startTime = System.currentTimeMillis();

    /*
        1: 生成1 - 10 的数字
        2: 创建10盘菜
        3: 提交runAsync 并且执行make
        4: 转换为数组
        5: 执行allOf
        6: 执行统一join
     */
    CompletableFuture.allOf(IntStream.range(1, 10)
            .mapToObj(i -> new Dish("菜" + i, 1))
            .map(dish -> CompletableFuture.runAsync(dish::make))
            .toArray(CompletableFuture[]::new)).join();

    SmallTool.print("菜都做好了, 上桌 " + (System.currentTimeMillis() - startTime));

}

瞬间一大片代码变成了一句, emmm, 我则么没有这么吊,当然执行结果是一样的

需求进化

如果小白现在突然想点20盘菜呢?
需求点:

  • 任务巨多, 如何保证性能
  • 如何观察任务, 调度情况
  • 线程复用问题

    实现

    编写代码

    其实就是将上一个例子的10改为20而已

    执行结果

    1649521040000    |    1    |    main    |    小白和小伙伴门 进餐厅点菜
    1649521041025    |    37    |    ForkJoinPool.commonPool-worker-29    |    菜14制作完毕 来吃我吧!
    1649521041025    |    30    |    ForkJoinPool.commonPool-worker-31    |    菜6制作完毕 来吃我吧!
    1649521041025    |    36    |    ForkJoinPool.commonPool-worker-11    |    菜13制作完毕 来吃我吧!
    1649521041025    |    27    |    ForkJoinPool.commonPool-worker-9    |    菜4制作完毕 来吃我吧!
    1649521041025    |    34    |    ForkJoinPool.commonPool-worker-7    |    菜11制作完毕 来吃我吧!
    1649521041025    |    31    |    ForkJoinPool.commonPool-worker-17    |    菜9制作完毕 来吃我吧!
    1649521041025    |    35    |    ForkJoinPool.commonPool-worker-25    |    菜12制作完毕 来吃我吧!
    1649521041025    |    24    |    ForkJoinPool.commonPool-worker-19    |    菜2制作完毕 来吃我吧!
    1649521041025    |    28    |    ForkJoinPool.commonPool-worker-27    |    菜5制作完毕 来吃我吧!
    1649521041025    |    25    |    ForkJoinPool.commonPool-worker-5    |    菜1制作完毕 来吃我吧!
    1649521041025    |    26    |    ForkJoinPool.commonPool-worker-23    |    菜3制作完毕 来吃我吧!
    1649521041025    |    29    |    ForkJoinPool.commonPool-worker-13    |    菜7制作完毕 来吃我吧!
    1649521041025    |    38    |    ForkJoinPool.commonPool-worker-15    |    菜15制作完毕 来吃我吧!
    1649521041025    |    33    |    ForkJoinPool.commonPool-worker-21    |    菜10制作完毕 来吃我吧!
    1649521041025    |    32    |    ForkJoinPool.commonPool-worker-3    |    菜8制作完毕 来吃我吧!
    1649521042040    |    30    |    ForkJoinPool.commonPool-worker-31    |    菜16制作完毕 来吃我吧!
    1649521042040    |    28    |    ForkJoinPool.commonPool-worker-27    |    菜18制作完毕 来吃我吧!
    1649521042040    |    26    |    ForkJoinPool.commonPool-worker-23    |    菜19制作完毕 来吃我吧!
    1649521042040    |    25    |    ForkJoinPool.commonPool-worker-5    |    菜17制作完毕 来吃我吧!
    1649521042048    |    1    |    main    |    菜都做好了, 上桌 2040
    

    可以看的出来, 执行的线程重复了, 用时2ms, 为什么呢? 核心池的最大是15, 应为这个和你电脑的CPU核心数有关, 我电脑是8核16线程的, ForkJoinPool的最大线程数 默认应该是最大线程数-1
    我们看一下

    @Test
    public void testForkJoinPool(){
      // 电脑支持的最大线程数
      System.out.println(Runtime.getRuntime().availableProcessors());
      // 通用池 当前大小
      System.out.println(ForkJoinPool.commonPool().getPoolSize());
      // 通用池最大线程数
      System.out.println(ForkJoinPool.getCommonPoolParallelism());
    }
    

    结果

    16
    0
    15
    

    这个时候就需要用到线程池来解决了,之前CompeletableFuture的方法总结中带Async的后缀的方法, 其实都是可以多传入一个参数的,那就是指定线程池, 如果不指定,默认使用的线程池就是ForkJoinPool.commonPool从名字也可以看出,这是ForkJoin的池

    改进一

    将通用池的线程数设置为合适大小 ```java @Test public void testThree(){

    // -Djava.util.concurrent.ForkJoinPool.common.parallelism=20 System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”, “20”);

    SmallTool.print(“小白和小伙伴门 进餐厅点菜”);

    long startTime = System.currentTimeMillis();

    CompletableFuture.allOf(IntStream.range(1, 20)

          .mapToObj(i -> new Dish("菜" + i, 1))
          .map(dish -> CompletableFuture.runAsync(dish::make))
          .toArray(CompletableFuture[]::new)).join();
    

    SmallTool.print(“菜都做好了, 上桌 “ + (System.currentTimeMillis() - startTime));

}

<a name="hr8Me"></a>
## 执行结果
```java
1649522125623    |    1    |    main    |    小白和小伙伴门 进餐厅点菜
1649522126645    |    35    |    ForkJoinPool.commonPool-worker-25    |    菜12制作完毕 来吃我吧!
1649522126645    |    34    |    ForkJoinPool.commonPool-worker-39    |    菜11制作完毕 来吃我吧!
1649522126645    |    28    |    ForkJoinPool.commonPool-worker-59    |    菜5制作完毕 来吃我吧!
1649522126645    |    41    |    ForkJoinPool.commonPool-worker-5    |    菜18制作完毕 来吃我吧!
1649522126645    |    29    |    ForkJoinPool.commonPool-worker-45    |    菜6制作完毕 来吃我吧!
1649522126645    |    40    |    ForkJoinPool.commonPool-worker-19    |    菜17制作完毕 来吃我吧!
1649522126645    |    24    |    ForkJoinPool.commonPool-worker-51    |    菜2制作完毕 来吃我吧!
1649522126645    |    26    |    ForkJoinPool.commonPool-worker-9    |    菜4制作完毕 来吃我吧!
1649522126645    |    25    |    ForkJoinPool.commonPool-worker-37    |    菜1制作完毕 来吃我吧!
1649522126645    |    37    |    ForkJoinPool.commonPool-worker-61    |    菜14制作完毕 来吃我吧!
1649522126645    |    27    |    ForkJoinPool.commonPool-worker-23    |    菜3制作完毕 来吃我吧!
1649522126645    |    33    |    ForkJoinPool.commonPool-worker-53    |    菜10制作完毕 来吃我吧!
1649522126645    |    32    |    ForkJoinPool.commonPool-worker-3    |    菜9制作完毕 来吃我吧!
1649522126645    |    42    |    ForkJoinPool.commonPool-worker-55    |    菜19制作完毕 来吃我吧!
1649522126645    |    36    |    ForkJoinPool.commonPool-worker-11    |    菜13制作完毕 来吃我吧!
1649522126645    |    30    |    ForkJoinPool.commonPool-worker-31    |    菜7制作完毕 来吃我吧!
1649522126645    |    38    |    ForkJoinPool.commonPool-worker-47    |    菜15制作完毕 来吃我吧!
1649522126645    |    39    |    ForkJoinPool.commonPool-worker-33    |    菜16制作完毕 来吃我吧!
1649522126645    |    31    |    ForkJoinPool.commonPool-worker-17    |    菜8制作完毕 来吃我吧!
1649522126649    |    1    |    main    |    菜都做好了, 上桌 1023

可以看到,又回到1ms了, 但是这个值到底要设置为多少才合适呢?
答案是都不合适
原因

  • 从名字可以看出ForkJoinPool, 显然这个池并不只为CompeletableFuture服务
  • 只有在启动之前,初始化的时候才可以设置
  • 需要从项目的长期使用量才可以得出

    改进二(推荐)

    自定义线程池, 这个时候,就又说会上面的第二个参数了,没错 ,那就是Executor
    为什么推荐呢?
    原因

  • 隔离, 防止影响其他使用ForkJoinPool的代码

  • 方便控制, ForkJoinPool在初始化后, 不可以修改, 但是自定义的线程池可以在任务数量来之后, 通过计算得出线程的数量

这里采用无上限线程池演示

@Test
public void testFour(){

    SmallTool.print("小白和小伙伴门 进餐厅点菜");

    long startTime = System.currentTimeMillis();

    // 创建线程池
    final ExecutorService threadPool = Executors.newCachedThreadPool();

    CompletableFuture.allOf(IntStream.range(1, 20)
            .mapToObj(i -> new Dish("菜" + i, 1))
            .map(dish -> CompletableFuture.runAsync(dish::make, threadPool))
            .toArray(CompletableFuture[]::new)).join();

    // 销毁
    threadPool.shutdown();

    SmallTool.print("菜都做好了, 上桌 " + (System.currentTimeMillis() - startTime));

}

执行结果

1649522607939    |    1    |    main    |    小白和小伙伴门 进餐厅点菜
1649522608962    |    42    |    pool-1-thread-19    |    菜19制作完毕 来吃我吧!
1649522608963    |    26    |    pool-1-thread-3    |    菜3制作完毕 来吃我吧!
1649522608963    |    25    |    pool-1-thread-2    |    菜2制作完毕 来吃我吧!
1649522608963    |    29    |    pool-1-thread-6    |    菜6制作完毕 来吃我吧!
1649522608963    |    31    |    pool-1-thread-8    |    菜8制作完毕 来吃我吧!
1649522608963    |    24    |    pool-1-thread-1    |    菜1制作完毕 来吃我吧!
1649522608963    |    30    |    pool-1-thread-7    |    菜7制作完毕 来吃我吧!
1649522608963    |    39    |    pool-1-thread-16    |    菜16制作完毕 来吃我吧!
1649522608962    |    40    |    pool-1-thread-17    |    菜17制作完毕 来吃我吧!
1649522608963    |    28    |    pool-1-thread-5    |    菜5制作完毕 来吃我吧!
1649522608962    |    27    |    pool-1-thread-4    |    菜4制作完毕 来吃我吧!
1649522608962    |    38    |    pool-1-thread-15    |    菜15制作完毕 来吃我吧!
1649522608962    |    34    |    pool-1-thread-11    |    菜11制作完毕 来吃我吧!
1649522608962    |    32    |    pool-1-thread-9    |    菜9制作完毕 来吃我吧!
1649522608962    |    37    |    pool-1-thread-14    |    菜14制作完毕 来吃我吧!
1649522608962    |    33    |    pool-1-thread-10    |    菜10制作完毕 来吃我吧!
1649522608962    |    36    |    pool-1-thread-13    |    菜13制作完毕 来吃我吧!
1649522608962    |    41    |    pool-1-thread-18    |    菜18制作完毕 来吃我吧!
1649522608962    |    35    |    pool-1-thread-12    |    菜12制作完毕 来吃我吧!
1649522608973    |    1    |    main    |    菜都做好了, 上桌 1028

没错,还是1ms, 但是通过名称可以看出, 使用了我们自定义的线程池