今天咱们来聊聊 Executors 创建线程池这事儿。简单总结一下Executors 提供了几种便捷的方式来创建线程池,它用起来挺方便,但也有一些潜在的问题。
一、常见用法
-
newFixedThreadPool
-
原理:创建一个固定大小的线程池。它会预先创建指定数量的线程,这些线程会一直存活,等待任务到来。当所有线程都在执行任务时,如果有新任务提交,任务会在一个无界阻塞队列(通常是 LinkedBlockingQueue)中等待,直到有线程空闲。
-
实际项目中的用法示例:
- 假设我们在开发一个小型的 Web 服务器,用于处理 HTTP 请求。我们知道服务器的硬件资源有限,并且对于并发请求的处理能力有一个大概的预估。比如,服务器的 CPU 核心数是 4,我们可以使用 newFixedThreadPool 创建一个大小为 4 的线程池来处理请求。
-
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WebServer {
private ExecutorService executorService;
public WebServer() {
// 创建一个固定大小为4的线程池
executorService = Executors.newFixedThreadPool(4);
}
public void handleRequest(Request request) {
executorService.execute(() -> {
// 模拟处理请求的过程,比如解析请求、查询数据库、生成响应等
System.out.println("正在处理请求: " + request.getUrl());
});
}
public void shutdown() {
executorService.shutdown();
}
}
-
在这个例子中,线程池大小固定为 4,就像是有 4 个服务员在餐厅里等待顾客(请求)。当有 4 个请求同时到来时,4 个线程就会同时处理。如果还有更多请求,它们会排队等待,直到有线程空闲。
-
newCachedThreadPool
-
原理:这个线程池的线程数量是动态变化的。如果有新任务提交,并且当前没有空闲线程,就会创建一个新线程来处理任务。如果线程空闲了一段时间(默认 60 秒),就会被回收。它使用的是 SynchronousQueue 作为任务队列,这种队列不存储任务,只是直接将任务交给线程。
-
实际项目中的用法示例:
- 考虑一个爬虫项目,我们需要从大量的网页中获取数据。每个网页的获取速度可能不同,而且网页的数量非常多。我们可以使用 newCachedThreadPool 来创建线程池。
-
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WebCrawler {
private ExecutorService executorService;
public WebCrawler() {
// 创建一个可缓存的线程池
executorService = Executors.newCachedThreadPool();
}
public void crawlPage(String url) {
executorService.execute(() -> {
// 模拟爬取网页的过程,比如发送HTTP请求、解析HTML等
System.out.println("正在爬取网页: " + url);
});
}
public void shutdown() {
executorService.shutdown();
}
}
-
在这个例子中,因为每个网页的爬取任务时间不确定,可能有的网页很快就能爬取完成,有的可能会因为网络等原因耗时较长。newCachedThreadPool 会根据任务的到来动态地创建线程,当一个网页爬取完成后,线程可以很快地被用于下一个任务,或者如果一段时间没有任务,线程就会被回收,这样就能够高效地利用资源。
-
newSingleThreadExecutor
-
原理:创建一个只有一个线程的线程池。所有提交的任务会按照提交的顺序在这个线程中依次执行。它内部使用了一个无界队列(LinkedBlockingQueue)来存储任务。
-
实际项目中的用法示例:
- 想象我们有一个日志记录系统,需要将各种不同模块产生的日志信息按顺序写入文件。我们就可以使用 newSingleThreadExecutor。
-
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LogWriter {
private ExecutorService executorService;
public LogWriter() {
// 创建一个单线程的线程池
executorService = Executors.newSingleThreadExecutor();
}
public void writeLog(String log) {
executorService.execute(() -> {
// 模拟将日志写入文件的过程
System.out.println("正在写入日志: " + log);
});
}
public void shutdown() {
executorService.shutdown();
}
}
-
在这个例子中,由于日志的写入顺序很重要,我们不能让多个线程同时写入文件,否则可能会导致日志混乱。newSingleThreadExecutor 就保证了所有的日志按照提交的顺序依次写入文件。
-
newScheduledThreadPool
-
原理:这个线程池主要用于定时或周期性地执行任务。它内部有一个延迟队列(DelayedWorkQueue),用于存储定时任务。可以通过一些方法(如 schedule、scheduleAtFixedRate 等)来设置任务的执行时间和周期。
-
实际项目中的用法示例:
- 假设我们在开发一个任务调度系统,需要每天定时备份数据库,并且每隔一段时间清理一些临时文件。我们可以使用 newScheduledThreadPool。
-
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TaskScheduler {
private ExecutorService executorService;
public TaskScheduler() {
// 创建一个可定时执行任务的线程池,大小为2(可以根据任务数量调整)
executorService = Executors.newScheduledThreadPool(2);
}
public void scheduleDatabaseBackup() {
// 每天凌晨2点备份数据库
executorService.scheduleAtFixedRate(() -> {
System.out.println("正在备份数据库");
}, getTomorrowAt2AM(), 24, TimeUnit.HOURS);
}
public void scheduleTempFileCleanup() {
// 每3小时清理一次临时文件
executorService.scheduleAtFixedRate(() -> {
System.out.println("正在清理临时文件");
}, 0, 3, TimeUnit.HOURS);
}
private long getTomorrowAt2AM() {
// 计算明天凌晨2点的时间戳(这里省略具体计算过程)
return 0L;
}
public void shutdown() {
executorService.shutdown();
}
}
-
在这个例子中,我们通过 newScheduledThreadPool 创建了一个线程池,并且使用它来安排数据库备份和临时文件清理这两个任务。线程池会根据我们设置的时间和周期,自动执行这些任务。
二、优势
-
简单方便
- 在实际项目开发中,时间就是金钱。Executors 提供了简单的工厂方法来创建不同类型的线程池,这使得开发者不需要深入了解线程池的复杂配置,就可以快速地在项目中应用线程池。比如在一些小型项目或者原型开发阶段,快速地使用 newFixedThreadPool 或者 newCachedThreadPool 来实现简单的并发功能,能够节省大量的时间和精力。
-
代码可读性高
- 当其他开发者查看代码时,通过使用 Executors 创建的线程池,能够很容易地理解代码的意图。例如,看到 newSingleThreadExecutor,就能够马上知道这里的任务是按照顺序执行的。这种清晰的代码结构有助于团队协作和代码维护。
-
满足常见场景需求
-
对于很多常见的并发场景,Executors 提供的这几种线程池类型基本能够满足需求。比如,对于固定数量的并发任务可以使用 newFixedThreadPool,对于任务数量不确定但希望高效利用资源的情况可以使用 newCachedThreadPool,对于需要顺序执行任务的场景可以使用 newSingleThreadExecutor,对于定时任务可以使用 newScheduledThreadPool。
-
三、劣势
-
资源管理问题
- 潜在的线程过多问题(以 newCachedThreadPool 为例) :在高负载的情况下,newCachedThreadPool 可能会创建大量的线程。比如在一个处理海量网络请求的系统中,如果请求量突然暴增,newCachedThreadPool 会不断地创建新线程来处理请求。每个线程都需要占用一定的系统资源,包括内存(用于线程栈等)和 CPU 时间(用于线程切换等)。如果线程数量过多,可能会导致系统资源耗尽,甚至出现内存溢出(OutOfMemoryError)的情况。
- 资源浪费问题(以 newFixedThreadPool 和 newSingleThreadExecutor 为例) :在 newFixedThreadPool 和 newSingleThreadExecutor 中,线程数量是固定的。如果在某些时段,任务量很少,这些线程就会处于空闲状态,白白占用系统资源。例如,在一个业务有高峰期和低谷期的系统中,低谷期时固定数量的线程池中的线程大部分时间都在等待任务,这就造成了资源的浪费。
-
队列策略不够灵活
- Executors 创建的线程池默认的队列策略可能不适合所有的场景。比如 newFixedThreadPool 和 newSingleThreadExecutor 使用的是无界队列(LinkedBlockingQueue)。在一些情况下,如果任务产生的速度远远大于线程处理的速度,无界队列会不断地存储任务,可能会导致内存占用过多。而在某些对任务队列有特殊要求的场景,如需要使用有界队列来限制任务数量,Executors 创建的线程池就不能很好地满足需求。
-
定制化程度有限
- 在一些复杂的项目中,可能需要对线程池进行更精细的定制。例如,需要设置线程的优先级、使用自定义的线程工厂来创建具有特定属性的线程,或者定义特殊的拒绝策略来处理当线程池无法接受新任务的情况。Executors 创建的线程池在这些方面的定制化程度相对较低,可能无法满足这些特殊的需求,这时候可能需要直接使用 ThreadPoolExecutor 来进行更细致的配置。