1.3 Java 线程池
Java 线程池(Thread Pool)是一种线程的使用模式,是一种 Java 并发编程机制。Java 线程池能够有效地管理线程,通过线程复用提升使用效率。当我们使用的线程一旦变多,特别在进行高性能测试时,线程池就是我们唯一的选择。
使用线程池在以下几个方面有着巨大优势:
- (1)线程创建和销毁:创建和销毁是非常昂贵的操作,线程池通过复用已经创建的线程,减少线程的创建和销毁的次数,提升了 Java 程序的性能,减少资源消耗。
- (2)线程管理:使用线程池可以处理不同的负载的任务,需要一种灵活且可靠的线程管理策略。首先我们需要把线程总数限制在一个合理的范围内,其次要根据负载搞低、任务特性,及时增加或减少使用的线程,并且能够及时回收不用的线程。线程池提供线程管理策略的模板,用以满足不同的需求场景。
- (3)提升可维护性:使用线程池可以将线程创建和管理细节隐藏,只暴露提交任务的 API,是代码更加便于维护。让测试工程师更加关注用例的细节,而不用过多关注多线程实现的细节。
Java 线程池提供了一种高效的方式实现多线程编程,解决了多线程使用中的各种问题,从而让性能测试更加稳定、可靠。通过使用线程池,性能测试人员可以更加利用多核机器 CPU 性能,编写更加高性能且安全稳定的测试用例。
1.3.1 Executors 创建线程池
java.util.concurrent.Executors 是 Java 标准库中非常重要的工具类,经常会用来创建几种常用类型的线程池。它提供了简单的创建方法,对于刚刚入门 Java 多线程编程的读者来说,通过 java.util.concurrent.Executors 提供的模板创建线程池是非常合适的选择。
在性能测试中,我们通常会用到 2 类 Java 线程池:固定线程线程池(Fixed Thread Pool)和缓存线程池(Cached Thread Pool)。
下面让我们来了解这两种线程池使用以及简单应用。
1.固定线程线程池
固定线程线程池调用创建的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
根据 Java doc 描述:改方法创建一个线程池,拥有固定的线程数、无边界(实际为 integer 最大值)共享任务队列。改线程池限制了最大运行的线程数,如果某个线程运行过程中因为异常导致终止,当有新的任务需要执行时,会有创建新的线程。关闭该线程池需要调用java.util.concurrent.ExecutorService#shutdown
方法。
这个方法只有一个 int 参数 nThread,也就是线程池线程数。如果我们想使用该方法创建线程池,只需要考虑使用场景到底需要多少线程即可。使用方法如下:
ExecutorService executorService = Executors.newFixedThreadPool(3);// 创建线程数为3的线程池
然后就是往线程池中提交任务,java.util.concurrent.ExecutorService 提供了 2 个方法用于往线程池提交任务。其中性能测试中最常用的是 java.util.concurrent.Executor#execute,参数类型 java.lang.Runnable。还有 java.util.concurrent.ExecutorService#submit() 方法,有三个重载方法,在性能测试中极少用到,通常自动化测试项目用的比较多,本章不再展开讲解。
固定线程线程池演示代码如下:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}实现Java 固定线程线程池
*/
public class FixedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);// 创建线程数为3的线程池
for (int i = 0; i < 4; i++) {// 循环4次
executorService.execute(new Runnable() {// 提交任务
@Override
public void run() {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 打印当前时间、线程名称
}
});
}
executorService.shutdown();// 关闭线程池
}
}
控制台输出:
1695555879091 Hello FunTester! pool-1-thread-3
1695555879091 Hello FunTester! pool-1-thread-1
1695555879091 Hello FunTester! pool-1-thread-2
1695555879194 Hello FunTester! pool-1-thread-3
进程已结束,退出代码为 0。
当然我们还可以使用 Lambda 语法使代码更加简洁:
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());//
});
我们看到前 3 个信息是在同一时间打印的,且线程均不相同。第四条信息打印时间戳比前三条多了 103ms,这与代码中 Thread.sleep(100);休眠的 100 毫秒是基本一致的。这里显示我们创建的线程池中 3 个线程,当在执行任务的前 100 毫秒在处理前 3 个任务,等到某个任务结束后,再执行第 4 个任务。
我们可以得出一个结论:当固定线程线程池的线程都在处理别的任务(繁忙状态),剩余的任务实在等待执行的过程中,实际上任务是再等待队列中。在实际使用场景中,限制线程池中最大的线程数会导致线程池有一个处理任务的上限。
固定线程线程池适用于具有固定数量、需要严格控制最大线程数的并发执行场景。固定线程线程池能够提供稳定的线程数量,使任务执行的速率更加均匀。在使用过程中,需要注意任务队列中的等待数量,防止超量积压导致任务从提交到最终执行延迟过大。
在性能测试中,通常使用固定线程线程池来执行线程模型中固定线程、需要最大线程数限制的动态线程场景。
2.缓存线程池
首先我们观察缓存线程池的创建方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
根据 Java doc 的描述:改线程池根据需要创建线程,但是会复用已经创建好的线程,适合执行大量的短期的任务。如果线程空闲超过设置 60 秒,则会被回收,若是长时间未使用,该线程池会回收所有线程资源。
这是一个无参的方法,但不意味着我们可以无限创建线程,该方法创建的缓存线程池最大可以创建java.lang.Integer#MAX_VALUE
个线程,但在实际的使用中,优先于系统限制和资源限制,能够创建的线程数远低于这个值。
在线程池的使用方面,缓存线程池是完全跟固定线程线程池一样的,有两种提交任务的方法,这里我们展示 java.util.concurrent.Executor#execute 方法,下面是演示 Demo:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}实现Java 缓存线程池
*/
public class CachedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();// 创建一个缓存线程池
for (int i = 0; i < 8; i++) {// 循环8次
// 提交任务
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 输出当前时间和线程名
});
}
executorService.shutdown();// 关闭线程池
}
}
下面是控制台输出:
1696727545116 Hello FunTester! pool-1-thread-6
1696727545116 Hello FunTester! pool-1-thread-1
1696727545116 Hello FunTester! pool-1-thread-3
1696727545116 Hello FunTester! pool-1-thread-2
1696727545116 Hello FunTester! pool-1-thread-7
1696727545116 Hello FunTester! pool-1-thread-4
1696727545116 Hello FunTester! pool-1-thread-8
1696727545116 Hello FunTester! pool-1-thread-5
进程已结束,退出代码为 0。
使用 Lambda 语法简化代码同固定线程线程池,这里不再展示。
可以看出,我们提交了 8 个任务到线程池,几乎在同一时刻(时间戳相同)任务均被执行,且执行的线程都不一样。缓存线程池总计创建了 8 个线程,分别执行不同的任务。
在下面例子中,我们设计了每个线程任务在提交之前均休眠不同的时间,这样可以很清晰展示缓存线程池线程复用的效果。演示代码如下:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}实现Java 缓存线程池
*/
public class CachedThreadPoolReuseDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();// 创建一个缓存线程池
for (int i = 0; i < 8; i++) {//
try {
Thread.sleep(i * 100);// 睡眠i*100毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 提交任务
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 输出当前时间和线程名
});
}
executorService.shutdown();// 关闭线程池
}
}
控制台输出:
1696728595770 Hello FunTester! pool-1-thread-1
1696728595873 Hello FunTester! pool-1-thread-2
1696728596074 Hello FunTester! pool-1-thread-2
1696728596376 Hello FunTester! pool-1-thread-2
1696728596777 Hello FunTester! pool-1-thread-2
1696728597281 Hello FunTester! pool-1-thread-2
1696728597880 Hello FunTester! pool-1-thread-2
1696728598580 Hello FunTester! pool-1-thread-2
进程已结束,退出代码为 0
可以看到缓存线程池总计创建了 2 个线程用来执行 8 个任务,因为后来的任务到达时,前一个任务已经执行结束,线程已经空闲下来,可以执行新的任务。
接下来通过下面的例子演示缓存线程池使用中,当线程被异常终止时,线程池如何处理。演示代码如下:
package org.funtester.performance.books.chapter01.section3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用{@link ExecutorService}实现Java 缓存线程池
*/
public class CachedThreadPoolAbortDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();// 创建一个缓存线程池
for (int i = 0; i < 2; i++) {//
try {
Thread.sleep(i * 150);// 睡眠i*100毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 提交任务
executorService.execute(() -> {
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " Hello FunTester! " + Thread.currentThread().getName());// 输出当前时间和线程名
throw new RuntimeException("线程异常");
});
}
executorService.shutdown();// 关闭线程池
}
}
控制台输出:
1696729397014 Hello FunTester! pool-1-thread-1
Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 线程异常
at org.funtester.performance.books.chapter01.section3.CachedThreadPoolAbortDemo.lambda$main$0(CachedThreadPoolAbortDemo.java:27)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
1696729397165 Hello FunTester! pool-1-thread-2
Exception in thread "pool-1-thread-2" java.lang.RuntimeException: 线程异常
at org.funtester.performance.books.chapter01.section3.CachedThreadPoolAbortDemo.lambda$main$0(CachedThreadPoolAbortDemo.java:27)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
进程已结束,退出代码为 0。
我们先忽略这两个报错信息,可以看到线程池总计创建了 2 个线程去执行 2 个任务。根据我们之前的所学内容,如果不报错的话,只需要创建 1 个线程即可以执行这 2 个任务。这也对应了 Java doc 描中,当线程池终的线程被终止时,若仍需要线程,就会创建新的线程执行任务。这个逻辑不仅使用缓存线程池,也适用于固定线程线程池,包括 1.4 节的自定义线程池。
FunTester 原创精华