关于线程池的面试题
- 问题
- 1. 为什么要使用线程池,线程池用什么用
- 2. 说说几种常见的线程池及使用场景
- 3. 线程池有哪几种工作队列
- 4. 怎么理解无界队列和有界队列
- 5. 线程池中的几种重要的参数及流程
- 6. 单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)
- 参考
问题
问题:
- 单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)
- 为什么要使用线程池,线程池用什么用
- 说说几种常见的线程池及使用场景
- 线程池有哪几种工作队列
- 怎么理解无界队列和有界队列
- 线程池中的几种重要的参数及流程
1. 为什么要使用线程池,线程池用什么用
帮我们合理的管理线程的生命周期,避免重复的创建及销毁大量的线程增加系统开销。
2. 说说几种常见的线程池及使用场景
- newFixedThreadPool(固定大小的线程池)
- newSingleThreadExecutor(单线程线程池)
- newCachedThreadPool(可缓存线程的线程池)
- newScheduledThreadPool
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
线程池特点:
- 核心线程数和最大线程数大小一样
-
keepAliveTime
为0 - 阻塞队列是
LinkedBlockingQueue
它是固定大小的线程池,其核心线程数和最大线程数大小一样。并且阻塞队列用的是LinkedBlockingQueue
,也就是说线程最大数这个参数失效了基本,所以不会出现外包线程的存在,所以也可以认为keepAliveTime
参数是一个摆设。除非allowCoreThreadTimeOut
方法的调用。
该线程池的工作机制是:
- 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
- 线程数等于核心线程数后,将任务加入阻塞队列
- 由于队列容量非常大(
Integer.MAX_VALUE
),可以一直加加加。(当线程池中的任务比较特殊时,比如关于数据库的长时间的IO操作,可能导致OOM)
- 由于队列容量非常大(
- 执行完任务的线程反复去队列中取任务执行
适用场景:
FixedThreadPool
适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程即可。一般Ncpu+1
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
线程池特点:
- 核心线程数和最大线程数大小一样且都是1
-
keepAliveTime
为0 - 阻塞队列是
LinkedBlockingQueue
该线程池的工作机制是:
- 线程池中没有线程时,新建一个线程执行任务
- 有一个线程以后,将任务加入阻塞队列,不停加加加
- 唯一的这一个线程不停地去队列里取任务执行
适用场景:
SingleThreadExecutor
适用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
线程池特点:
- 核心线程数为0,且最大线程数为
Integer.MAX_VALUE
- 阻塞队列是
SynchronousQueue
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
锁当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。
该线程池的工作机制是:
- 没有核心线程,直接向
SynchronousQueue
中提交任务 - 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
- 执行完任务的线程有60秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜
适用场景:
CachedThreadPool
用于并发执行大量短期的小任务。
newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
线程池特点:
- 最大线程数为
Integer.MAX_VALUE
- 阻塞队列是
DelayedWorkQueue
ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:
- scheduleAtFixedRate() :按某种速率周期执行
- scheduleWithFixedDelay():在某个延迟后执行
两种方法的内部实现都是创建了一个ScheduledFutureTask
对象封装了任务的延迟执行时间及执行周期,并调用decorateTask()
方法转成RunnableScheduledFuture
对象,然后添加到延迟队列中。
DelayQueue:中封装了一个优先级队列,这个队列会对队列中的ScheduledFutureTask
进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的ScheduledFutureTask
的顺序号 sequenceNumber
,先提交的先执行。
该线程池的工作机制是:
- 调用上面两个方法添加一个任务
- 线程池中的线程从 DelayQueue 中取任务
- 然后执行任务
具体执行步骤:
- 线程从 DelayQueue 中获取 time 大于等于当前时间的
ScheduledFutureTask
DelayQueue.take()
- 执行完后修改这个 task 的 time 为下次被执行的时间
- 然后再把这个 task 放回队列中
DelayQueue.add()
适用场景:
ScheduledThreadPoolExecutor
用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
3. 线程池有哪几种工作队列
- ArrayBlockingQueue (有界队列):是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue (无界队列):一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue(同步队列): 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- DelayQueue(延迟队列):一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。
- PriorityBlockingQueue(优先级队列): 一个具有优先级得无限阻塞队列。
4. 怎么理解无界队列和有界队列
- 有界队列即长度有限,满了以后ArrayBlockingQueue会插入阻塞。
- 无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞,但是可能会出现OOM异常。
5. 线程池中的几种重要的参数及流程
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
-
corePoolSize
:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中 -
maximumPoolSize
:线程池最大线程数最大线程数 -
keepAliveTime
:表示线程没有任务执行时最多保持多久时间会终止 -
unit
:参数keepAliveTime的时间单位TimeUtil类的枚举类(DAYS、HOURS、MINUTES、SECONDS 等) -
workQueue
:阻塞队列,用来存储等待执行的任务 -
threadFactory
:线程工厂,主要用来创建线程 -
handler
:拒绝处理任务的策略-
AbortPolicy
:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种) -
DiscardPolicy
:也是丢弃任务,但是不抛出异常 -
DiscardOldestPolicy
:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) -
CallerRunsPolicy
:由调用线程处理该任务
-
执行流程
- 当有任务进入时,线程池创建线程去执行任务,直到核心线程数满为止
- 核心线程数量满了之后,任务就会进入一个缓冲的任务队列中
- 当任务队列为无界队列时,任务就会一直放入缓冲的任务队列中,不会和最大线程数量进行比较
- 当任务队列为有界队列时,任务先放入缓冲的任务队列中,当任务队列满了之后,才会将任务放入线程池,此时会拿当前线程数与线程池允许的最大线程数进行比较,如果超出了,则默认会抛出异常。如果没超出,然后线程池才会创建线程并执行任务,当任务执行完,又会将缓冲队列中的任务放入线程池中,然后重复此操作。
6. 单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)
经过网上查阅,发现基本是没有一个明确的回答的。不过思考过后一番,我感觉实现思路和MySQL的redo,undo功能很相似,我们可以对正在处理和阻塞队列的任务做事物管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的
已经执行成功的操作。然后重新执行整个阻塞队列。
即:
阻塞队列持久化,正在处理事物控制。断电之后正在处理的回滚,日志恢复该次操作。服务器重启后阻塞队列中的数据再加载