1.Executors创建的问题
对于线程池的创建,在一开始学习javaSE使用的是Executors类里的静态方法,但是这里创建线程池会有很多问题。
对于 newFixedThreadPool
和 newSingleThreadExecutor方法
这两个方法在创建线程池时使用的是 LinkedBlockingQueue
作为任务队列,这个队列的默认大小为 Integer.MAX_VALUE
。如果任务提交速度远高于处理速度,队列会不断堆积任务,最终导致内存溢出(OOM)。
对于newCachedThreadPool
和 newScheduledThreadPool
这两个方法默认线程数Integer.MAX_VALUE
,如果任务量激增,会创建大量线程,导致系统资源耗尽。
2.正确的创建方法
为了避免潜在的风险,更好地管理线程池,阿里巴巴开发手册建议使用 ThreadPoolExecutor
来手动创建线程池,以便根据具体的业务需求灵活配置线程池的参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
对应的参数介绍:
①. corePoolSize & maximumPoolSize
核心线程数(corePoolSize)和最大线程数(maximumPoolSize)是线程池中非常重要的两个概念。
当新任务提交时:
-
如果
当前运行线程数
<corePoolSize
,即使有空闲线程,也会创建新线程执行任务。 -
若
当前运行线程数
≥corePoolSize
,任务先进【队列等待】。 -
只有队列已满 &
当前运行线程数
< maximumPoolSize,才会新建线程。
②. keepAliveTime & unit
通过这种机制,线程池能够在任务负载减少时自动减少线程数量,从而节省系统资源。
-
keepAliveTime 为超过 corePoolSize 线程数量的线程最大空闲时间;
-
当线程池中的线程数量超过 corePoolSize 时,如果有多余的线程处于空闲状态(即没有任务可执行),这些空闲线程不会立即被销毁。keepAliveTime 指定了这些空闲线程在被终止之前可以等待新任务的最长时间。
-
unit 为时间单位。
-
具体的:当一个空闲线程在 ThreadPoolExecutor 中执行完一个任务后,如果该线程没有立即接收到新的任务,它会进入空闲状态。此时,keepAliveTime 会重新开始计时。
具体来说,keepAliveTime 的计时逻辑如下:
-
任务执行完毕:线程执行完一个任务后,会检查任务队列中是否有新的任务可执行。
-
进入空闲状态:如果没有新的任务可执行,线程会进入空闲状态。
-
重新计时:进入空闲状态后,keepAliveTime 开始计时。如果在 keepAliveTime 规定的时间内没有新的任务到达,该线程将被终止(前提是该线程数量超过 corePoolSize,或者 allowCoreThreadTimeOut(true) 被调用)。
-
新任务到达:如果在 keepAliveTime 内有新任务到达,线程会被重新激活以执行新任务,keepAliveTime 的计时会在下次进入空闲状态时重新开始。
③. 等待队列(workQueue)
用于存放等待执行任务的队列。当前运行的线程数量超过 corePoolSize
时,新任务会被放入该队列中等待执行。
下面来看一下三种通用的入队策略:
- 无界队列(LinkedBlockingQueue):
-
特点:队列容量无限(Integer.MAX_VALUE),任务可以一直添加到队列中。也因此,不会触发 maximumPoolSize。
-
适用场景:适用于任务量大但不希望频繁创建线程的场景,比如 Tomcat。
-
注意事项:如果任务提交速度远高于处理速度,队列会无限增长,最终导致内存溢出(OOM)。
-
- 有界队列 (ArrayBlockingQueue):
-
特点:队列容量固定,任务数超过容量时会触发创建新线程(如果线程数未达最大线程数)。
-
适用场景:适用于控制并发量,避免资源耗尽。
-
注意事项:队列容量需要根据业务需求合理设置,过小可能导致频繁触发拒绝策略。
-
- 同步队列 (SynchronousQueue):
-
特点:不存储任务,直接将任务交给空闲线程处理;如果没有空闲线程,则创建新线程(如果线程数未达最大线程数)。
-
适用场景:任务量较小且需要快速响应的场景,例如高并发短任务处理。
-
注意事项:线程不足时,任务可能被拒绝。
-
④. 线程工厂 (threadFactory)
用于创建线程的工厂类,通过它可以自定义线程的创建方式,如设置线程的名称、优先级、是否为守护线程等。
⑤. 拒绝策略 (handler)
当任务队列已满且线程数量达到 maximumPoolSize
时,新提交的任务会被拒绝。
下面来看一下四种常见的拒绝策略:
-
AbortPolicy(默认):直接抛出 RejectedExecutionException 异常。
-
CallerRunsPolicy:由提交任务的线程直接执行该任务
-
DiscardPolicy:直接丢弃新任务,不抛异常。
-
DiscardOldestPolicy:丢弃任务队列中最老的任务,然后尝试重新提交被拒绝的任务。
一个例子:
public static void main(String[] args) {
// 创建一个线程池,核心线程数为2,最大线程数为4,空闲线程存活时间为10秒
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
4,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 提交任务给线程池
for (int i = 0; i < 6; i++) {
final int taskNumber = i;
executor.execute(() -> {
System.out.println("任务 " + taskNumber + " 正在由线程 " + Thread.currentThread().getName() + " 执行");
try {
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("任务 " + taskNumber + " 执行完毕");
});
}
// 关闭线程池
executor.shutdown();
try {
// 等待所有任务完成
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
执行结果: