面试题:线程池常见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: 谁提交任务,谁来处理。将任务交给提交任务的线程执行(一般是主线程)。

    好处:

    1. 新提交的任务不会被丢弃,不会造成数据丢失。
    2. 执行任务通常比较耗时,既可以延迟新任务的提交,又可为执行其他任务腾出一点时间。

四、 有哪 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问重要的主要内容,如果未能解决你的问题,请参考以下文章

2021最新版大厂面试题线程池一池多问连环炮,绝对干货!!!

金九银十:线程多线程,线程池面试题十连问!

面试经验|常见的字符串常量池必问面试题

面试题:线程基础5问

如图两道面试题,顺便深入线程池,并连环17问

整理的70道阿里高级Java面试题,都来挑战一下,看看自己有多厉害