线程池是一种管理多个线程的工具,用于优化线程的使用,避免频繁创建和销毁线程带来的性能损耗。它通过复用线程来提高系统资源利用率和任务处理效率,是 Java 并发编程中重要的部分。
线程池的核心思想
复用线程
- 通过维护一个线程集合(线程池),在有任务时分配线程,无任务时线程处于等待状态。
减少创建和销毁线程开销
- 避免频繁创建和销毁线程,降低资源开销。
任务排队
- 当所有线程都忙时,将任务放入队列等待。
Java 中的线程池
Java 提供了 java.util.concurrent 包下的线程池实现,主要通过 Executor 框架来管理。
核心类和接口
Executor
- 线程池顶层接口,定义了基本的任务提交方法。
ExecutorService
- 扩展了 Executor,增加了管理线程池生命周期的方法,如 shutdown()、awaitTermination()。
ThreadPoolExecutor
- 线程池的具体实现类,提供了高度可配置的线程池
Executors
- 工具类,用于创建常见类型的线程池。
工作流程
当任务提交到线程池
- 如果有空闲线程,任务直接交由空闲线程执行
- 如果没有空闲线程,且线程池并未到达最大值,创建新的线程执行任务
- 如果线程数到达最大值,直接进入等待队列
- 如果等待队列也满了,根据策略处理任务(如拒绝任务,抛出异常)
核心参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程的存活时间,多余线程在此时间后销毁
- workQueue:用于存储任务的等待队列
- threadFactory:创建线程的工厂类,通常用于定义线程属性
- hanlder:拒绝策略
常见的线程池类型
通过Executors工具类可以快速创建以下线程池:
固定线程池(newFixedThreadPool)
- 固定数量的线程,适用于稳定负载。
- 示例:ExecutorService executor = Executors.newFixedThreadPool(5);
缓存线程池(newCachedThreadPool)
- 动态调整线程数量,适用于大量短时任务
- 示例:ExecutorService executor = Executors.newCachedThreadPool();
单线程池(newSingleThreadExecutor)
- 单线程串行执行任务。
- 示例:ExecutorService executor = Executors.newSingleThreadExecutor();
定时线程池(newScheduledThreadPool)
- 执行延迟或周期性任务。
- 示例:ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
自定义线程池
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
1, TimeUnit.MINUTES, // 空闲线程存活时间
new LinkedBlockingQueue<>(10), // 队列容量
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
for (int i = 0; i < 20; i++) {
int task = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is executing task " + task);
});
}
threadPool.shutdown();
}
}
线程池优点
- 降低资源消耗
通过复用线程减少频繁创建和销毁线程的开销。 - 提高响应速度
任务可以直接复用现有线程处理,无需等待线程创建。 - 提升管理能力
可以限制线程数量,避免因资源争抢导致的系统崩溃。
线程池的注意事项
- 任务提交后无法取消
阻塞队列的选择
- 有界队列需配合拒绝策略处理超出容量的任务
- 无界队列(LinkedBlockingQueue)可能导致最大线程数失效
- 避免任务执行时间过长
任务执行时间过长会阻塞线程,降低线程池吞吐量。 - 避免资源泄漏
使用 shutdown() 及时关闭线程池。
线程池拒绝策略
当线程池无法接收新任务时(如队列满,线程池关闭),会触发拒绝策略。Java 提供了以下内置策略:
- AbortPolicy
抛出 RejectedExecutionException。 - CallerRunsPolicy
由调用线程执行任务。 - DiscardPolicy
丢弃任务,不抛异常。 - DiscardOldestPolicy
丢弃队列中最旧的任务,然后尝试重新提交
import java.util.concurrent.*;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
Runnable task = () -> System.out.println("Task executed at: " + System.currentTimeMillis());
// 延迟 2 秒后执行
scheduler.schedule(task, 2, TimeUnit.SECONDS);
// 延迟 1 秒后,每隔 3 秒执行一次
scheduler.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);
// 延迟 1 秒后,间隔 3 秒执行任务,但以任务执行结束时间为间隔
scheduler.scheduleWithFixedDelay(task, 1, 3, TimeUnit.SECONDS);
}
}
向线程池提交任务的方式
常用的方式包括提交可运行的任务(Runnable)、可调用的任务(Callable)、批量任务以及定时任务。
- 使用 execute() 方法提交任务,不关心任务的返回结果
- 使用 submit() 方法提交任务,提交任务后会返回一个 Future 对象,可通过它获取任务的执行结果或监控任务状态。支持 Runnable 和 Callable 两种任务类型
批量提交:使用线程池的 invokeAll() 和 invokeAny() 方法一次性提交多个 Callable 任务。
- invokeAll():返回所有任务的 Future 对象,阻塞直到所有任务完成。
- invokeAny():返回最快完成任务的结果,其他任务取消。
- 提交定时任务:使用 ScheduledExecutorService 提交定时或周期性任务。
关闭线程池
shutdown()
- 发出线程池的关闭请求,不再接受新任务,但会继续执行已经提交的任务,包括在队列中的任务。
- 线程池中的线程在完成所有任务后会被销毁
注意:
- 如果需要确保线程池已完全关闭,可以调用 awaitTermination() 进行阻塞等待
shutdownNow()
- 试图停止正在执行的任务并返回未执行的任务列表,同时不再接受新任务
- 使用此方法可能会导致任务中断,但具体是否中断取决于任务代码的设计(是否响应中断)。
线程池如何合理设置
理解任务类型
CPU密集型任务
- 特点:主要使用 CPU 进行计算,例如科学计算、加密解密、图像处理等
- 推荐大小:线程池大小应接近于 CPU 核心数。
- 公式:CPU+1
I/O密集型任务
- 特点:任务包含大量 I/O 操作,例如网络请求、文件读写等,线程通常处于等待状态。
- 推荐大小:线程池大小应远大于 CPU 核心数
- 原因:增加线程数可以充分利用等待时间,提高吞吐量。
混合型任务
- 同时包含 CPU 密集和 I/O 密集操作。
- 解决方法:拆分任务为 CPU 密集型和 I/O 密集型,分别用不同线程池处理。
系统资源约束
- 内存消耗:线程池中的每个线程会占用一定的内存(主要是栈空间,通常为1MB),过多的线程可能导致内存溢出。
- 上下文切换:线程数过多会增加上下文切换开销,导致性能下降。
使用任务队列管理任务
线程池的任务队列可以配置不同类型来影响线程池的行为:
直接提交队列(SynchonousQueue)
特点:每次提交任务都需要有线程立即处理,否则无法提交。
- 适用场景:需要严格控制线程数,任务执行速度与线程池速度匹配。
无界队列(LinkedBlockingQueue)
特点:队列大小无限制,新任务可以持续进入,线程数固定。
- 适用场景:任务执行时间短,吞吐量大。
有界队列(ArrayBlockingQueue)
队列大小固定,超出时需要等待或拒绝任务。
- 需要限制内存占用,防止任务堆积
注意事项
- 避免死锁:设置合理的队列大小和线程数,防止任务相互依赖导致死锁。
选择拒绝策略:
- AbortPolicy:抛出异常。
- CallerRunsPolicy:在提交任务的线程中执行任务。
- DiscardPolicy:丢弃任务,不抛异常。
- DiscardOldestPolicy:丢弃最早的任务。
- 测试和优化:通过负载测试不断调整线程池大小。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。