Jakob Jenkov 2023-01-24
线程池是一组可以“复用”来执行任务的线程,使得每个线程可以执行多个任务。线程池是为每个任务都创建新线程的一种替代方案。
相比于为每个任务都创建一个新线程,复用已存在的线程能显著减少性能开销。因此,使用已有线程来执行任务通常可以获得更高的整体吞吐量。
此外,使用线程池可以更方便地控制同时活跃的线程数量。每个线程都会消耗一定的系统资源(例如内存 RAM),如果同时激活的线程过多,可能导致系统资源耗尽(比如操作系统因内存不足而开始将内存交换到磁盘),从而导致计算机变慢。
在本线程池教程中,我将解释线程池的工作原理、使用场景,以及如何在 Java 中实现一个线程池。需要注意的是,Java 已经内置了线程池——即 java.util.concurrent.ExecutorService,所以你无需自己从头实现。不过,有时你可能希望自定义线程池,以支持 ExecutorService 所不具备的功能;或者,你也可以把实现自己的线程池当作一次学习体验。
线程池的工作原理
线程池不会为每个并发任务都启动一个新线程,而是将任务提交给线程池。只要池中有空闲线程,任务就会被分配给其中一个并执行。
内部实现上,这些任务会被插入到一个 阻塞队列(Blocking Queue)中,而线程池中的线程会不断地从该队列中取出任务执行。当一个新任务被加入队列时,某个空闲线程会成功将其取出并执行,其余空闲线程则继续阻塞等待下一个任务。

线程池的使用场景
线程池常用于多线程服务器中。每当有新的网络连接到达服务器时,该连接会被封装成一个任务,并提交给线程池。线程池中的线程会并发处理这些连接上的请求。
后续教程将详细介绍如何在 Java 中实现多线程服务器。
Java 内置的线程池
Java 在 java.util.concurrent 包中已经提供了内置的线程池,因此你不需要自己实现。
尽管如此,了解线程池的内部实现仍然是有益的。
Java 线程池的实现
下面是一个简单的线程池实现。该实现使用了 Java 5 起就提供的标准 java.util.concurrent.BlockingQueue。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ThreadPool {
private BlockingQueue<Runnable> taskQueue = null;
private List<PoolThreadRunnable> runnables = new ArrayList<>();
private boolean isStopped = false;
public ThreadPool(int noOfThreads, int maxNoOfTasks) {
taskQueue = new ArrayBlockingQueue<>(maxNoOfTasks);
for (int i = 0; i < noOfThreads; i++) {
PoolThreadRunnable poolThreadRunnable = new PoolThreadRunnable(taskQueue);
runnables.add(poolThreadRunnable);
}
for (PoolThreadRunnable runnable : runnables) {
new Thread(runnable).start();
}
}
public synchronized void execute(Runnable task) throws Exception {
if (this.isStopped)
throw new IllegalStateException("ThreadPool is stopped");
this.taskQueue.offer(task);
}
public synchronized void stop() {
this.isStopped = true;
for (PoolThreadRunnable runnable : runnables) {
runnable.doStop();
}
}
public synchronized void waitUntilAllTasksFinished() {
while (this.taskQueue.size() > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以下是 PoolThreadRunnable 类,它实现了 Runnable 接口,因此可以被 Java 线程执行:
import java.util.concurrent.BlockingQueue;
public class PoolThreadRunnable implements Runnable {
private Thread thread = null;
private BlockingQueue<Runnable> taskQueue = null;
private boolean isStopped = false;
public PoolThreadRunnable(BlockingQueue<Runnable> queue) {
taskQueue = queue;
}
public void run() {
this.thread = Thread.currentThread();
while (!isStopped()) {
try {
Runnable runnable = taskQueue.take();
runnable.run();
} catch (Exception e) {
// 记录或报告异常,但保持线程存活
}
}
}
public synchronized void doStop() {
isStopped = true;
// 中断线程,使其从 taskQueue.take() 的阻塞中退出
this.thread.interrupt();
}
public synchronized boolean isStopped() {
return isStopped;
}
}
最后,这是使用上述 ThreadPool 的示例:
public class ThreadPoolMain {
public static void main(String[] args) throws Exception {
ThreadPool threadPool = new ThreadPool(3, 10);
for (int i = 0; i < 10; i++) {
int taskNo = i;
threadPool.execute(() -> {
String message = Thread.currentThread().getName() + ": Task " + taskNo;
System.out.println(message);
});
}
threadPool.waitUntilAllTasksFinished();
threadPool.stop();
}
}
实现说明
该线程池实现包含两个部分:
ThreadPool类:对外暴露的线程池接口;PoolThreadRunnable类:代表实际执行任务的线程。
执行任务
调用 ThreadPool.execute(Runnable r) 方法,传入一个 Runnable 实例。该任务会被加入内部的阻塞队列,等待空闲线程取出执行。
任务执行流程
PoolThreadRunnable.run() 方法会不断从队列中取出任务并执行。执行完一个任务后,线程会循环尝试获取下一个任务,直到线程池被停止。
停止线程池
调用 ThreadPool.stop() 方法会设置 isStopped 标志,并对每个工作线程调用 doStop()。这会中断每个线程(通过 thread.interrupt()),使其从 taskQueue.take() 的阻塞状态中退出。此时线程会捕获 InterruptedException,检查 isStopped 标志,发现为 true 后便退出 run() 方法,线程随之终止。
注意:execute() 方法在 stop() 被调用后会抛出 IllegalStateException,防止向已关闭的线程池提交新任务。