手动创建线程池通过ThreadPoolExecutor配置核心参数,如corePoolSize、maximumPoolSize、workQueue等,实现灵活控制;而Executors工具类提供newFixedThreadPool、newCachedThreadPool等快捷方式,但可能因使用无界队列或无限线程数导致OOM。推荐手动创建以避免资源耗尽风险,并根据CPU核心数、任务类型(CPU或IO密集型)合理设置线程池大小,结合压力测试调整参数。关闭线程池时应先调用shutdown(),再通过awaitTermination()等待任务完成,必要时调用shutdownNow()强制终止,确保资源正确释放。

创建线程池的方式主要有手动创建和使用Executors工具类创建两种。手动创建可以更灵活地配置线程池参数,而Executors则提供了一些预定义的线程池,方便快捷。
手动创建和Executors工具类。
如何手动创建线程池?
手动创建线程池的核心在于使用ThreadPoolExecutor类。你需要仔细配置它的几个关键参数:
- corePoolSize: 核心线程数,即使线程池中没有任务执行,也会保持存活的线程数量。这可以减少任务提交时的延迟。
- maximumPoolSize: 线程池允许的最大线程数。当任务队列满了,且当前线程数小于maximumPoolSize时,线程池会创建新的线程来执行任务。
- keepAliveTime: 当线程池中的线程数量大于corePoolSize时,多余的空闲线程在终止之前等待新任务的最长时间。
- unit: keepAliveTime的时间单位,例如TimeUnit.SECONDS。
-
workQueue: 用于保存等待执行的任务的队列。常见的选择有:
-
LinkedBlockingQueue: 无界队列,可能导致OOM。 -
ArrayBlockingQueue: 有界队列,可以防止OOM,但需要合理设置队列大小。 -
SynchronousQueue: 不存储任务,直接提交给线程执行,如果线程池中没有空闲线程,则创建新线程,或者根据拒绝策略处理。
-
- threadFactory: 用于创建线程的工厂,可以自定义线程的名称、优先级等。
-
rejectedExecutionHandler: 当任务无法被执行时,使用的拒绝策略。常见的策略有:
-
AbortPolicy: 抛出RejectedExecutionException异常。 -
CallerRunsPolicy: 由调用线程执行该任务。 -
DiscardPolicy: 直接丢弃该任务。 -
DiscardOldestPolicy: 丢弃队列中最老的任务,然后尝试执行当前任务。
-
一个简单的例子:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 60;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue workQueue = new ArrayBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, rejectedExecutionHandler);
for (int i = 0; i < 10; i++) {
final int taskNumber = i;
executor.execute(() -> {
System.out.println("Task " + taskNumber + " executed by " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown(); //不再接受新的任务
try {
executor.awaitTermination(10, TimeUnit.SECONDS); // 等待所有任务完成,最多等待10秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Finished all threads");
}
} Executors工具类提供了哪些线程池?
Executors类提供了一些静态方法来创建预定义的线程池,简化了线程池的创建过程,但也隐藏了一些细节,需要谨慎使用:
-
newFixedThreadPool(int nThreads): 创建一个固定大小的线程池。如果所有线程都在忙,新的任务会在队列中等待。使用了
LinkedBlockingQueue,队列长度无限制,可能导致OOM。 -
newCachedThreadPool(): 创建一个可缓存的线程池。线程池的大小不固定,可以根据需要动态增加或减少线程。空闲线程会被回收(默认60秒),新任务会复用之前空闲的线程。使用了
SynchronousQueue,每个插入操作必须等待另一个线程的对应移除操作,所以线程数理论上可以无限增加,也可能导致OOM。 -
newSingleThreadExecutor(): 创建一个单线程的线程池。所有任务都会按照FIFO的顺序执行。使用了
LinkedBlockingQueue,队列长度无限制,可能导致OOM。 - newScheduledThreadPool(int corePoolSize): 创建一个可以执行定时任务的线程池。
使用示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
final int taskNumber = i;
executor.execute(() -> {
System.out.println("Task " + taskNumber + " executed by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有任务完成
}
System.out.println("Finished all threads");
}
}为什么推荐手动创建线程池,而不是使用Executors?
虽然Executors简化了线程池的创建,但它隐藏了关键的配置细节,可能导致一些问题,特别是OOM。
-
newFixedThreadPool和newSingleThreadExecutor使用了无界的LinkedBlockingQueue,如果任务提交速度超过处理速度,队列会无限增长,最终导致OOM。 -
newCachedThreadPool使用了SynchronousQueue,允许创建无限数量的线程,如果任务提交速度过快,可能会创建大量的线程,耗尽系统资源,导致OOM或系统崩溃。
手动创建线程池可以让你更精确地控制线程池的参数,例如选择合适的队列类型和大小,设置合理的线程数量和keepAliveTime,从而避免这些潜在的问题。
如何选择合适的线程池大小?
线程池大小的选择是一个需要权衡的问题,没有一个固定的最佳值。通常需要考虑以下因素:
- CPU核心数: 这是最基本的参考。对于CPU密集型任务,线程池的大小通常设置为CPU核心数+1。这样可以最大限度地利用CPU资源,同时避免过多的上下文切换。
-
任务类型:
- CPU密集型任务: 线程池大小 = CPU核心数 + 1
- IO密集型任务: 线程池大小 = CPU核心数 (1 + IO阻塞时间 / CPU计算时间)。 由于IO操作会阻塞线程,因此可以增加线程数量,提高CPU利用率。另一种经验法则是:线程池大小 = 2 CPU核心数。
- 内存大小: 线程池中的每个线程都需要一定的内存空间。如果线程池过大,可能会导致内存不足。
- 任务的响应时间要求: 如果任务的响应时间要求很高,可以适当增加线程数量,减少任务等待时间。
- 系统的负载情况: 如果系统负载已经很高,不宜创建过大的线程池,否则可能会导致系统崩溃。
可以使用一些工具来监控线程池的性能,例如JConsole、VisualVM等。通过监控线程池的活跃线程数、队列长度、任务执行时间等指标,可以更好地了解线程池的运行状况,并根据实际情况进行调整。
千博购物系统.Net能够适合不同类型商品,为您提供了一个完整的在线开店解决方案。千博购物系统.Net除了拥有一般网上商店系统所具有的所有功能,还拥有着其它网店系统没有的许多超强功能。千博购物系统.Net适合中小企业和个人快速构建个性化的网上商店。强劲、安全、稳定、易用、免费是它的主要特性。系统由C#及Access/MS SQL开发,是B/S(浏览器/服务器)结构Asp.Net程序。多种独创的技术使
一般来说,可以通过以下步骤来选择合适的线程池大小:
- 确定任务类型: CPU密集型还是IO密集型。
- 估算任务的平均执行时间: 可以通过benchmark测试或者实际运行数据来估算。
- 根据公式计算初始的线程池大小: 根据任务类型选择合适的公式。
- 进行压力测试: 使用不同的线程池大小进行压力测试,观察系统的性能指标,例如响应时间、吞吐量、CPU利用率等。
- 根据测试结果进行调整: 根据压力测试的结果,逐步调整线程池的大小,找到一个最佳的平衡点。
选择合适的线程池大小是一个迭代的过程,需要不断地监控和调整。
如何优雅地关闭线程池?
正确地关闭线程池非常重要,可以避免资源泄漏和程序异常退出。ExecutorService提供了两个方法来关闭线程池:
- shutdown(): 停止接受新的任务,但会等待所有已经提交的任务执行完成。
- shutdownNow(): 尝试停止所有正在执行的任务,并停止处理正在等待的任务。返回等待执行的任务列表。
一般来说,推荐使用shutdown()方法来关闭线程池,因为它允许正在执行的任务完成,可以避免数据丢失或状态不一致。但是,如果需要立即关闭线程池,可以使用shutdownNow()方法。
在调用shutdown()或shutdownNow()方法后,可以使用awaitTermination()方法来等待所有任务完成。awaitTermination()方法会阻塞当前线程,直到所有任务完成或者超时。
executor.shutdown(); // 拒绝接受新的任务
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 等待60秒
executor.shutdownNow(); // 如果超时,强制关闭
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未能终止");
}
}
} catch (InterruptedException ie) {
executor.shutdownNow(); // 发生中断,强制关闭
Thread.currentThread().interrupt();
}这段代码首先调用shutdown()方法来拒绝接受新的任务,然后调用awaitTermination()方法来等待所有任务完成,最多等待60秒。如果在60秒内所有任务都完成了,那么线程池就正常关闭了。如果超时,那么调用shutdownNow()方法来强制关闭线程池,并再次调用awaitTermination()方法来等待正在执行的任务停止。如果在第二次等待超时后,线程池仍然未能终止,那么就打印一条错误信息。
需要注意的是,即使调用了shutdownNow()方法,也可能有一些任务无法立即停止,例如正在进行IO操作的任务。
另外,如果任务在执行过程中抛出了异常,可能会导致线程池中的线程提前终止。为了避免这种情况,可以在任务的run()方法中使用try-catch块来捕获异常,并进行处理。









