Java线程池面试题

一、什么是线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。

如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。


二、为什么要使用线程池?

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。(我们可以把创建和销毁的线程的过程去掉)


三、线程池有什么作用?

线程池作用就是限制系统中执行线程的数量。

1、提高效率
创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。

2、方便管理
可以编写线程池管理代码对池中的线程同一进行管理,比如说启动时有该程序创建 100 个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有 101 个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。


四、几种常见的线程池及使用场景

1、newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序 (FIFO, LIFO, 优先级) 执行。

2、newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3、newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

4、newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。


五、线程池中的几种重要的参数

  • corePoolSize 就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收

  • maximumPoolSize 就是线程池中可以容纳的最大线程的数量

  • keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清 除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,

  • util,就是计算这个时间的一个单位。

  • workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是 FIFIO 原则(先进先出)。

  • threadFactory,就是创建线程的线程工厂。

  • handler, 是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。


六、使用线程池注意事项

  • 避免使用无界队列
    不要使用 Executors.newXXXThreadPool() 快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免 OOM,我们应该使用 ThreadPoolExecutor 的构造方法手动指定队列的最大长度:
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
                new ThreadPoolExecutor.DiscardPolicy());
  • 明确拒绝任务时的行为
    任务队列总有占满的时候,这是再 submit() 提交新的任务会怎么样呢?RejectedExecutionHandler 接口为我们提供了控制方式,接口定义如下:
public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

七、execute 和 submit 的区别?

在前面的讲解中,我们执行任务是用的 execute 方法,除了 execute 方法,还有一个 submit 方法也可以执行我们提交的任务。

这两个方法有什么区别呢?分别适用于在什么场景下呢?我们来做一个简单的分析。

  • execute 适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。

  • submit 方法适用于需要关注返回值的场景,线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get () 方法来获取返回值,get () 方法会阻塞当前线程直到任务完成


八、五种线程池的使用场景

  • newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。

  • newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。使用的任务队列是LinkedBlockingQueue

  • newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。使用的任务队列是SynchronousQueue

  • newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。

  • newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用 cpu 数量的线程来并行执行。


九、线程池的拒绝策略

当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。RejectedExecutionHandler 接口提供了拒绝任务处理的自定义方法的机会。在 ThreadPoolExecutor 中已经包含四种处理策略。

  • AbortPolicy 策略:该策略会直接抛出异常,阻止系统正常工作。

  • CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的任务。

  • DiscardOleddestPolicy 策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

  • DiscardPolicy 策略:该策略默默的丢弃无法处理的任务,不予任何处理。
    在这里插入图片描述
    除了 JDK 默认提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现 RejectedExecutionHandler 接口即可。

线程池默认的拒绝行为是 AbortPolicy,也就是抛出 RejectedExecutionHandler 异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成 DiscardPolicy,这样多余的任务会悄悄的被忽略。


十、线程池的关闭

关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现

  • shutdownNow:对正在执行的任务全部发出 interrupt (),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。

  • shutdown:当我们调用 shutdown 后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

发布了147 篇原创文章 · 获赞 835 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/qq_33945246/article/details/102552889