面试题:线程池常见10问重要
Posted 格子衫111
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试题:线程池常见10问重要相关的知识,希望对你有一定的参考价值。
一、使用线程池比手动创建线程好在哪里?
1、减少线程生命周期带来的开销。如:线程是提前创建好的,可以直接使用,避免创建线程的消耗。
2、合理的利用内存和CPU。如:避免线程创建较多造成的内存溢出,避免线程创建较少造成CPU的浪费。
3、可以统一管理资源。如:统一管理任务队列,可以统一开始或结束任务。
/**
* 例子: 用固定线程数的线程池执行10000个任务
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10000; i++) {
service.execute(new Task());
}
System.out.println(Thread.currentThread().getName());
}
static class Task implements Runnable {
public void run() {
System.out.println("Thread Name: " + Thread.currentThread().getName());
}
}
}
二、线程池的各参数的含义?
-
corePoolSize:核心线程数(“长工”),常驻线程的数量。随着任务增多,线程池从0开始增加。
-
maxPoolSize:最大线程数,创建线程的最大容量。是核心线程数与非核心线程数之和。
-
keepAliveTime+时间单位:空闲线程存活时间。当非核心线程(“临时工”)空闲时,过了存活时间该线程就会被- 回收 。
-
ThreadFactory:创建线程的工厂。
-
workQueue:存放任务的队列。任务队列满了,会创建非核心线程,直至达到最大线程数。
-
Handler:任务拒绝策略。当线程数达到最大,并且队列被塞满时,会拒绝任务。
三、线程池有哪 4 种拒绝策略?
newThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
new ThreadPoolExecutor.DiscardOldestPolicy());
拒绝任务的时机:
1.线程池关闭的时候,如调用shutdown方法。
2.超出线程池的任务处理能力。线程数量达到最大,且任务队列已满。
4种拒绝策略:
-
AbortPolicy:抛出RejectedExecutionException 的 RuntimeException异常,可根据业务做重试或做放弃提交等处理。
-
DiscardPolicy:直接丢弃任务不做任何提示,存在数据丢失风险。
-
DiscardOldestPolicy: 丢弃任务头节点,通常是存活时间最长的任务。给新提交的任务让路,这样也存在一定的数据丢失风险。
-
CallerRunsPolicy: 谁提交任务,谁来处理。将任务交给提交任务的线程执行(一般是主线程)。
好处:
- 新提交的任务不会被丢弃,不会造成数据丢失。
- 执行任务通常比较耗时,既可以延迟新任务的提交,又可为执行其他任务腾出一点时间。
四、 有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?
-
FixedThreadPool: 固定线程池。核心线程数由构造参数传入,最大线程数=核心线程数。
-
CachedThreadPool: 缓存线程池。核心线程数为0,最大线程数为 2^31-1 。 队列的容量为0 ( SynchronousQueue )。
-
ScheduledThreadPool: 定时线程池。可延迟x秒,可延迟x秒,按y周期执行(起始点有开始或结束)。
-
SingleThreadPool:单一线程池。和FixedThreadPool差不多,区别在于只有一个核心线程数。
-
SingleThreadScheduledPool: 单一定时线程池。和ScheduledThreadPool差不多,区别在于内部只有一个线程。
-
ForkJoinPool(java8才有):拆分汇总线程池。特点1:任务可以再次分裂子任务,并且可以汇总子任务的数据。特点2:每个任务都有一个自己的阻塞队列,而非共同拥有一个线程池阻塞队列。常用于递归场景(树的遍历,最有路径搜索)。
创建线程池示例:
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
service.schedule(new Task(), 10, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);
service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);
五、 线程池常用的阻塞队列有哪些?
线程池内部结构 :
1.线程池管理器:负责线程创建、销毁、添加任务等;
2.工作线程: 线程池创建的正在工作的线程;
3.任务队列( BlockingQueue ):线程满了之后,可以放到任务队列中,起到一定的缓冲;
4.任务:要求实现统一的接口,方便处理和执行;
线程池的阻塞队列:
-
LinkedBlockingQueue:容量大小为 Integer.MAX_Value,无界队列。对应线程池有 FixedThreaPool、SingleThreadPool;
-
SynchronousQueue:容量大小为0。对应线程池有ChachedThreadPool(可理解线程数无限扩展);
-
DelayedWorkQueue: 延迟工作队列。队列中的任务不是按照任务存放的先后顺序放的,而是按照延迟时间的先后存放的。对应线程池有ScheduledThreadPool、SingleThreadScheduledPool。
六、为什么不应该自动创建线程池?
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
-
FixedThreadPool、SingleThreadPool:使用的是无界队列(LinkedBlockingQueue),当任务堆积很多时,会占用大量内存,最终导致OOM。
-
ChachedTheadPool:可以无限创建线程(Integer.MAX_VALUE),任务过多时会导致创建线程达到操作系统上线或者发生OOM。
-
ScheduledThreadPool、SingleThreadScheduledPool:使用的是DelayedWorkQueue队列,实质上也是一种无界队列,会导致OOM。
七、 合适的线程数量是多少?CPU 核心数和线程数的关系?
- CPU密集型任务:
占用CPU比较多的任务(加密、解密、计算等),最佳线程数为CPU核心数的 1~2倍。
- 耗时IO型任务:
IO耗时比较多的任务(数据库、文件读写、网络传输等),占用CPU较少。《Java并发编程实战》推荐:最佳线程数= CPU核心数*(1+线程平均等待时间/线程平均工作时间)。
ps:线程平均工作时间越长,应创建较少的线程。线程平均等待时间长,应创建较多的线程。
八、 如何根据实际需要,定制自己的线程池?
-
核心线程数:平均工作时间比例多 ,定义较少的线程数;平均等待时间比例高,创建较多的线程数。如一个任务CPU密集和IO耗时混搭,最大线程数应为核心线程数的几倍,应对突发情况。
-
阻塞队列: 相对于无界队列,可使用 ArrayBlockingQueue,可以设置固定容量,防止资源耗尽,同时会产生数据丢失。
另外,队列容量大小和最大线程数应做一个平衡。队列容量大,最大线程数小时,可减少上下文切换,但是减少吞吐量。队列容量小,最大线程数大时,可提高效率,但是增多上下文切换。
-
线程工厂: 我们可以使用默认的 defaultThreadFactory, 也可以使用 ThreadFactoryBuilder创建 线程工厂,并自定义线程名。
ThreadFactoryBuilder factoryBuilder = new ThreadFacoryBuillder(); ThreadFactory threadFactory = builder.setNameFormat("rpc-pool-%d").build();
这样,线程名会为 rpc-pool-1,rpc-pool-2…
-
拒绝策略:除了4种常规拒绝策略,还可以自定义拒绝策略,做日志打印, 暂存任务、重新执行等操作 。实现方式,继承 RejecedExecutionHandler 接口,重写 rejectedExecution () 方法。
private static class CustomRejectionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//打印日志、暂存任务、重新执行等拒绝策略
}
}
九、如何正确关闭线程池?shutdown 和 shutdownNow 的区别?
-
shutdown():调用此方法,线程池不会马上关闭,会等线程运行完 ,并且阻塞的任务运行完再关闭。
-
isShutdown():判断线程池是否被标记关闭。调用了shutdown方法后,此方法会返回true。
-
isTerminated():判断线程池中是否已关闭并且阻塞的任务都已执行完 。
-
awaitTermination():判断线程池终结状态。等待周期内,线程池终结会返回true。超过等待时间,线程池未终结会返回false。等待周期内,线程被中断会抛出InterruptedException异常。
-
shutdownNow():表示立即关闭线程池。会向所有线程发送中断信号,并停止线程。将等待中的任务转移到list中,以后可做补救措施。
十、 线程池实现“线程复用”的原理?
核心原理是线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()创建一个新线程。而是会让每个线程去执行一个”循环任务“,这个”循环任务“会去检查是否存在等待执行的任务(通过firstTask 或者getTask),如果存在,则直接调用任务的run()方法进行任务的执行,把run()当作一个普通方法调用,这样线程数量并不会增加。
线程池 execute 方法 源码:
public void execute(Runnable command) {
//如果传入的Runnable的空,就抛出异常
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判断当前线程数是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
//在线程池中创建一个线程并执行.参数1:任务,参数2:true 代表增加线程时判断当前线程是否少于 corePoolSize,小于则增加新线程,大于等于则不增加
if (addWorker(command, true))
return;
c = ctl.get();
}
//当前线程数大于或等于核心线程数或者 addWorker 失败
//检查线程池状态是否为 Running
//线程池状态是 Running 就把任务放入任务队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//线程池已经不处于 Running 状态
//移除刚刚添加到任务队列中的任务
if (! isRunning(recheck) && remove(command))
//执行拒绝策略
reject(command);
//线程池状态为 Running
//检查当前线程数为 0
else if (workerCountOf(recheck) == 0)
//执行 addWorker() 方法新建线程
addWorker(null, false);
}
//线程池不是 Running 状态或线程数大于或等于核心线程数并且任务队列已经满了,参数1:任务,参数2:false 代表增加线程时判断当前线程是否少于 maxPoolSize,小于则增加新线程,大于等于则不增加
else if (!addWorker(command, false))
//执行拒绝策略 reject 方法
reject(command);
}
Worker 类中的 run 方法里执行的 runWorker 方法 简化源码:
//Worker 可以理解为是对 Thread 的包装,Worker 内部有一个 Thread 对象,它正是最终真正执行任务的线程
runWorker(Worker w) {
Runnable task = w.firstTask;
//通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务
while (task != null || (task = getTask()) != null) {
try {
//直接调用 Runnable 的 run 方法来执行任务
task.run();
} finally {
task = null;
}
}
}
以上是关于面试题:线程池常见10问重要的主要内容,如果未能解决你的问题,请参考以下文章