FunTester 【连载 15】线程池选择

FunTester · 2025年02月06日 · 67 次阅读

3.2 线程池选择

线程池作为压测引擎的核心执行器,是构建整个方案的重中之重。第 1 章我们已经讲过了线程池的常见类型以及适用场景,这里不多赘述。因为我们选择的是线程模型,为了更好的管理线程及任务,我们选择自定义线程池。设计线程池参数考虑以下几点:

  • 保障足够线程资源执行测试用例。
  • 保障测试任务提交后快速被执行。
  • 保障线程复用,避免测试过程中频繁创建和销毁线程。
  • 保障不会创建远超需求的线程。

这些条件对应到线程池参数上,可以参考 java.util.concurrent.Executors#newFixedThreadPool(int) 方法的内容:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

这里做几点修改:

  • 将线程 keepAliveTime 调整为 60 秒,这样可以提升线程复用机会。
  • 我们增加 threadFactory 参数,方便我们打印日志和排查故障。
  • workQueue 我们限制最大长度,设置为 1,或者使用 java.util.concurrent.SynchronousQueue 代替 java.util.concurrent.LinkedBlockingQueue

演示代码如下:

package org.funtester.performance.books.chapter03.section2;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ThreadFactory 演示类
 */
public class ThreadFactoryDemo {

    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactory() {// 创建一个线程工厂

            AtomicInteger index = new AtomicInteger();// 线程安全的线程编号

            @Override
            public Thread newThread(Runnable r) {// 重写创建线程方法
                Thread thread = new Thread(r);// 创建线程
                thread.setName("线程-" + index.incrementAndGet());// 设置线程名称
                return thread;// 返回创建的线程
            }
        };
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), threadFactory);// 创建线程池
        for (int i = 0; i < 8; i++) {// 向线程池提交8个任务
            threadPoolExecutor.execute(new Runnable() {// 提交任务
                @Override
                public void run() {// 任务执行逻辑
                    System.out.println(System.currentTimeMillis() + "  线程池中的线程名称: " + Thread.currentThread().getName());// 输出线程名称
                    try {
                        Thread.sleep(100);// 模拟任务执行时间
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);// 抛出运行时异常
                    }
                }
            });
        }
        threadPoolExecutor.shutdown();// 关闭线程池
    }
}

控制台输出:

1699978852559  线程池中的线程名称: 线程-1
1699978852559  线程池中的线程名称: 线程-2
1699978852559  线程池中的线程名称: 线程-3
1699978852663  线程池中的线程名称: 线程-3
1699978852664  线程池中的线程名称: 线程-2
1699978852664  线程池中的线程名称: 线程-1
1699978852768  线程池中的线程名称: 线程-3
1699978852769  线程池中的线程名称: 线程-2

为了让每一个线程的名字都不一样,笔者在 ThreadFactory 实现中增加了 AtomicInteger 对象,用于对创建线程进行线程安全的计数。我们创建了最大线程数为 3 的线程池,然后利用线程工厂设置了每个线程的名字,且具有唯一性。为了展示线程复用效果,增加了 workQueue 的容量,避免提交任务时被拒绝。根据打印信息可以得出实际效果符合预期的结论。

在使用的过程中,如果线程在执行任务遭遇中断情况,会重新创建新的线程补充到线程池中,相应的线程工厂中的计数器就会增加,大于线程池最大线程数。

以上线程池的选择基于线程模型,若是选择 TPS 模型,一般我们会尽量选择最大线程数远大于核心线程数,workQueue 容量非常小或者使用 java.util.concurrent.SynchronousQueue,下面是一个简单的例子。

package org.funtester.performance.books.chapter03.section2;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class TheadPoolForTpsModel {

    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactory() {// 创建一个线程工厂

            AtomicInteger index = new AtomicInteger();// 线程安全的线程编号

            @Override
            public Thread newThread(Runnable r) {// 重写创建线程方法
                Thread thread = new Thread(r);// 创建线程
                thread.setName("线程-" + index.incrementAndGet());// 设置线程名称
                return thread;// 返回创建的线程
            }
        };
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 200, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), threadFactory);// 创建线程池

        for (int i = 0; i < 8; i++) {// 向线程池提交8个任务
            threadPoolExecutor.execute(new Runnable() {// 提交任务
                @Override
                public void run() {// 任务执行逻辑
                    System.out.println(System.currentTimeMillis() + "  线程池中的线程名称: " + Thread.currentThread().getName());// 输出线程名称
                    try {
                        Thread.sleep(100);// 模拟任务执行时间
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);// 抛出运行时异常
                    }
                }
            });
        }
        threadPoolExecutor.shutdown();// 关闭线程池
    }
}

控制台输出:

1700012277892  线程池中的线程名称: 线程-1
1700012277893  线程池中的线程名称: 线程-4
1700012277892  线程池中的线程名称: 线程-3
1700012277892  线程池中的线程名称: 线程-2
1700012277893  线程池中的线程名称: 线程-6
1700012277893  线程池中的线程名称: 线程-5
1700012277893  线程池中的线程名称: 线程-7
1700012277893  线程池中的线程名称: 线程-8

可以线程池创建了 8 个线程去执行任务。在实际工作中,最大线程数会比例子中设置的 200 还要大,在预估最大线程数时要悲观一些,尽量取较大的线程数。虽然遭遇线程数达到最大线程数时依旧不够用的时候,可以通过线程池的 API 动态调整线程池配置,但是非常不优雅,而且操作难度比较高,不建议新手使用。

书的名字:从 Java 开始做性能测试

如果本书内容对你有所帮助,希望各位不吝赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。

FunTester 原创精华

【连载】从 Java 开始性能测试

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册