风离不摆烂学习日志Day17 Java 异步线程池 测压

前情提要:

为了验证上一篇所提到的线程池工具类 在实际业务场景下的可行性 设计了 多个对照组 使用Jmeter测压软件 完成对比实验

实验流程

image-20230109151253465

对比组一 串行组


    @RequestMapping("/testSync")
    public void test() throws InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("开始执行任务");
        // 1s
        Thread.sleep(1000);
        System.out.println("第一个任务执行完成");
        // 3s
        Thread.sleep(3000);
        System.out.println("第二个任务执行完成");
        // 5s
        Thread.sleep(5000);
        System.out.println("第三个任务执行完成");
        // 2s
        Thread.sleep(2000);
        System.out.println("第四个任务执行完成");
        System.out.println("任务执行完成,耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

串行组耗时 11s

image-20230109142406981

现在我们使用Jmeter 分别创建 20 个 200 个 250个 并发线程 看看结果 (PS: 查询资料可知 tomcat默认并发数(最大请求数为200)

20个

image-20230109145022827

200个

image-20230109145254550

这个是上一次结果累加的到的 可以看到 目前一切正常

250个

image-20230109145428216

这里可以清楚的看到 Tomcat默认的并发数 的确是200 超过两百的 就会排队 等候之前的 线程执行完毕之后才会去执行剩余线程 这次平均时间为 13s

串行组小结

  1. 验证了Tomcat的默认最大请求数为200
  2. 为异步组提供了对照 (控制变量法

对比组二 异步并行组

    private final ThreadPoolExecutor executor = ExecutorUtils.getThreadPoolExecutorInstance(); // 线程组
    @RequestMapping("/testAsync")
    public void testAsync() throws InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("开始执行任务");

        // 1s
        CompletableFuture futureOne = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第一个异步任务执行完成");
            return null;
        }, executor);


        // 3s
        CompletableFuture futureTwo = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第二个异步任务执行完成");
            return null;
        }, executor);

        // 5s
        CompletableFuture futureThree = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第三个异步任务执行完成");
            return null;
        }, executor);
        //需要等待所有异步任务执行完成 才能执行下面的同步任务

        CompletableFuture[] completableFutures = Arrays.asList(futureOne, futureTwo,futureThree).toArray(new CompletableFuture[0]);
        ExecutorUtils.batchExec(completableFutures); //批量执行

        // 2s
        Thread.sleep(2000);
        System.out.println("第四个同步任务执行完成");

        System.out.println("任务执行完成,耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

异步并行组耗时7s

image-20230109154005740

image-20230109153902422

同时我们可以看到 设置的最大cpu核数为 16核 核心CPU数 为 8*0.75 = 12核

同样的 我们分别模拟 20 个 200 个 250个 并发线程 看看结果

20

image-20230109152021861

这里出现了一个很有意思的现象:

image-20230109152707494

这个接口 的耗时逐渐增加

最开始 第一个请求是 7s 最后一个请求的时间达到了 19s

image-20230109152932694

这里来分析一下这个问题出现的原因:

image-20230109153118837

我们可以看到前4个请求的 执行耗时 几乎都是 7s 从第五个开始逐渐 每多一个请求 请求时长 多 1s

改造一下代码 打印一下 线程Id 和Name 看看 是否合理运用了

   @RequestMapping("/testAsync")
    public void testAsync() throws InterruptedException {
        long start = System.currentTimeMillis();
        System.out.println("开始执行任务");

        // 1s
        CompletableFuture futureOne = CompletableFuture.supplyAsync(() -> {
            try {
                System.out.println();
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("futureOne "+Thread.currentThread().getId()+"---"+Thread.currentThread().getName()+"---"+"第一个异步任务执行完成");
            return null;
        }, executor);


        // 3s
        CompletableFuture futureTwo = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("futureTwo "+Thread.currentThread().getId()+"---"+Thread.currentThread().getName()+"---"+"第二个异步任务执行完成");
            return null;
        }, executor);

        // 5s
        CompletableFuture futureThree = CompletableFuture.supplyAsync(() -> {
            try {

                System.out.println();
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //获取当前线程名称
            System.out.println("futureThree "+Thread.currentThread().getId()+"---"+Thread.currentThread().getName()+"---"+"第三个异步任务执行完成");
            return null;
        }, executor);
        //需要等待所有异步任务执行完成 才能执行下面的同步任务

        CompletableFuture[] completableFutures = Arrays.asList(futureOne, futureTwo,futureThree).toArray(new CompletableFuture[0]);
        ExecutorUtils.batchExec(completableFutures); //批量执行

        // 2s
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getId()+"---"+Thread.currentThread().getName()+"---"+"第四个同步任务执行完成");

        System.out.println("任务执行完成,耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

image-20230109155224623

同样起20个线程

image-20230109155448536

可以看到 这个线程的最大线程号 就是我们设置 的最大 核心cpu数 12

目前我们知道 只有12个线程在执行任务 且最耗时的任务为 5s 即其中一个线程必定会在 5s 之后才会执行下一个任务 再来看看 刚刚的超过4个请求之后 耗时就增加1秒 我们可以很容易的分析出 原因 4*3 =12 核 吃满了12个线程 没有多余的线程去处理异步任务 所以他们陷入了等待 假设不考虑执行损耗 我们可以列出如下表格

thread 1 =》1s

thread 2 =》3s

thread 3 =》5s


thread 4 =》1s

thread 5 =》3s

thread 6 =》5s


thread 7 =》1s

thread 8 =》3s

thread 9 =》5s


thread 10 =》1s

thread 11 =》3s

thread 12 =》5s

所以第二轮请求会在 1s之后才开始 所以 每次都累加1s(有线程还在被占用中)

所以就目前来看 公共的线程池 显然是个错误的方向 我们只利用到了一个线程池(12线程)这里我也认识到了之前犯的错误 目前的解决方案 需要 充分利用到所有线程池 即默认的200个

代码优化 充分利用200个线程池

上述例子: 理论上 充分利用上200个线程池 (以我电脑配置来举例)最多同时处理 200*4 = 800个并发请求


  /**
     * 获取线程池 初始化
     * @return
     */
    public static ThreadPoolExecutor getThreadPoolExecutorInstance() {

        //每一次都新建
        return new ThreadPoolExecutor(corePoolSize,
                            maxPoolSize, keepAliveTime,
                            java.util.concurrent.TimeUnit.SECONDS,
                            new java.util.concurrent.ArrayBlockingQueue<Runnable>(queueCapacity),
                            new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()); //.ThreadPoolExecutor.CallerRunsPolicy() 的意思是:如果线程池已经满了,那么就由调用者所在的线程来执行任务

    }

20个

image-20230109163823308

可以看到现在耗时正常了 也充分利用到了各个线程池

image-20230109163855799

200个

image-20230109164048033

可以看到一切正常

250个 预测一波不会有任何问题

image-20230109164850203

不出所料

由刚刚得到结论可知 我的电脑最大能处理 200*4 = 800 个并发请求

所以这次直接上850个线程

image-20230109165344728

image-20230109165506533

image-20230109165518692

jmeter 测试显示数据 跟打印有很大偏差(Tomcat的排队等待机制 200一批) 这里以实际请求为主 可以看到850线程还是能稳定 7s

当然要继续测试下去 找到波动的点 这次直接上3000线程

image-20230109165952962

牛逼。。。还是全部耗时7s

那 1w呢 想来继续测试下去也没有意义了 因为一次只会 200个 多的都排队了…

到现在 基本上可以得到一些结论了

结论

  1. 线程池与线程之间的关系: 线程池是线程的集合 Tomcat默认最大创建200个线程池
  2. 最大并发数取决于服务器的配置 CPU核数 以及某接口中异步任务个数(这个会占用线程 而核心线程数又是由CPU决定的 故得出此结论)(某一接口并发)具体公式为 、(cpu核心数/异步任务个数)* 200(Tomcat默认 可调)
  3. 特别的 像那种1w 10w 这种高并发 其实都是分批进行的 比如并发数为800 他就会800一批来跑

后续 测试恶劣情况下 可能会出现的问题(如请求报错 超时 触发拒绝策略等…)