前言
大家好,我是田螺。
我们来看一道京东一面面试题:Java 提供哪几种线程池,什么场景使用?
- newFixedThreadPool
- newCachedThreadPool
- newSingleThreadExecutor
- newScheduledThreadPool
1. newFixedThreadPool
newFixedThreadPool的构造函数:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
1.1 线程池特点:
- 核心线程数和最大线程数大小一样
- 没有所谓的非空闲时间,即keepAliveTime为0
- 阻塞队列为无界队列LinkedBlockingQueue
1.2 newFixedThreadPool工作机制
图片
- 提交任务
- 如果线程数少于核心线程,创建核心线程执行任务
- 如果线程数已经等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
- 如果线程执行完任务,去阻塞队列取任务,继续执行。
- 如果持续无限添加任务,可能会导致OOM,因为它是无界队列。
1.3 无界队列OOM的实例代码
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}
为了验证OOM,IDE指定JVM参数:-Xmx8m -Xms8m
运行结果:
图片
newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM:
图片
大家有兴趣可以看看源码哈:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
...
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
...
}
1.4 使用场景
- 定时任务调度
对于需要定时执行的任务,如每天的报表生成、数据备份或清理任务,FixedThreadPool 可以保持固定数量的线程来按时执行这些任务,确保系统在高峰期也能稳定运行。
- 一些后台服务中,比如邮件发送、短信通知等
在一些后台服务中,比如邮件发送、短信通知等,使用 FixedThreadPool 可以确保有足够的线程来处理发送请求,而不会因为突发的高并发请求导致系统崩溃。例如,在一个活动结束后,用户会收到活动总结邮件,固定线程池可以有效管理邮件发送任务,确保每封邮件都能及时发送。
- 适用于处理CPU密集型的任务
CPU密集型任务是指那些主要依赖于CPU计算能力的任务。这类任务通常需要大量的计算资源,且其执行时间与CPU的处理能力密切相关。与之相对的是I/O密集型任务,后者主要受限于输入/输出操作(如磁盘读写、网络请求等)。
2. newCachedThreadPool
newCachedThreadPool 的构造函数。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
2.1 线程池特点
- 核心线程数为0
- 最大线程数为Integer.MAX_VALUE
- 阻塞队列是SynchronousQueue
- 非核心线程空闲存活时间为60秒
当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。
2.2 newCachedThreadPool工作机制
图片
- 提交任务
- 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
- 判断是否有空闲线程,如果有,就去取出任务执行。
- 如果没有空闲线程,就新建一个线程执行。
- 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。
2.3 无界队列OOM的实例代码
newCachedThreadPool 使用不当,也是会导致OOM的,比如以下这个demo:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolOOMExample {
public static void main(String[] args) {
// 创建一个无界线程池
ExecutorService executorService = Executors.newCachedThreadPool();
try {
// 不断提交任务,模拟内存消耗
while (true) {
executorService.submit(() -> {
// 模拟一个长时间运行的任务
try {
// 创建一个大的对象来消耗内存
int[] largeArray = new int[1000000]; // 1,000,000 integers
// 模拟一些计算
for (int i = 0; i < largeArray.length; i++) {
largeArray[i] = i;
}
// 让线程稍微休眠一下
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
2.4 使用场景
使用用于用于并发执行大量短期的小任务。比如一些网络爬虫、Web服务器处理请求。
3. newSingleThreadExecutor
newSingleThreadExecutor的构造函数:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3.1 线程池特点
- 核心线程数为1
- 最大线程数也为1
- 阻塞队列是LinkedBlockingQueue
- keepAliveTime为0
3.2 newSingleThreadExecutor的工作机制
图片
- 提交任务
- 线程池是否有一条线程在,如果没有,新建线程执行任务
- 如果有,讲任务加到阻塞队列
- 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。
3.3 newSingleThreadExecutor的实例代码
newSingleThreadExecutor 使用的也是无界队列。如果任务提交速率过高,可能会导致系统资源耗尽(如内存溢出)。我们来看一个简单使用demo:
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()+"正在执行");
});
}
运行结果:
图片
3.4 使用场景
适用于串行执行任务的场景,一个任务一个任务地执行。比如任务调度
在某些业务场景中,任务之间存在依赖关系,即一个任务的输出是另一个任务的输入。在这种情况下,使用单线程执行器可以确保任务按预期的顺序执行。
4. newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
4.1 线程池特点
- 最大线程数为Integer.MAX_VALUE
- 阻塞队列是DelayedWorkQueue
- keepAliveTime为0
- scheduleAtFixedRate() :按某种速率周期执行
- scheduleWithFixedDelay():在某个延迟后执行
4.2 工作机制
- 添加一个任务
- 线程池中的线程从 DelayQueue 中取任务
- 线程从 DelayQueue 中获取 time 大于等于当前时间的task
- 执行完后修改这个 task 的 time 为下次被执行的时间
- 这个 task 放回DelayQueue队列中
4.3 实例代码
/**
创建一个给定初始延迟的间隔性的任务,之后的下次执行时间是上一次任务从执行到结束所需要的时间+* 给定的间隔时间
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleWithFixedDelay(()->{
System.out.println("current Time" + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"正在执行");
}, 1, 3, TimeUnit.SECONDS);
运行结果:图片
/**
创建一个给定初始延迟的间隔性的任务,之后的每次任务执行时间为 初始延迟 + N * delay(间隔)
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(()->{
System.out.println("current Time" + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"正在执行");
}, 1, 3, TimeUnit.SECONDS);;
周期性执行任务的场景,需要限制线程数量的场景。比如定时清理任务:
在某些应用程序中,可能会产生临时文件或日志记录。为了保持系统的整洁和性能,需要定期清理这些临时文件或日志。可以使用 newScheduledThreadPool 来安排清理任务,例如每小时或每天清理一次。