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 原创精华