深入浅出Java多线程(十三)之ThreadPool(下)

385 阅读4分钟

这是我参与8月更文挑战的第25天,活动详情查看:8月更文挑战

前言

关于ThreadPool的使用在网上其实也有挺全的,特别是Java,今天就来总结一下关于ThreadPool的常见面试题,顺便也可以巩固一下,并以此为切入点,谈谈我对线程池的理解。

ThreadPool

什么是线程池?

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

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

为什么要用线程池

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

线程池参数

  • corePoolSize:线程池核心线程数,即使线程空闲,也不会被回收销毁
  • maximumPoolSize:线程池最大线程数,即超过核心线程数,额外开辟的最大线程
  • keepAliveTime:除了核心线程外,额外开辟的线程的最大存活时间,超过即被回收销毁
  • workQueue:用于传输和保存等待执行任务的阻塞队列,超过最大线程外的线程,被塞进该队列等待,执行FIFO原则
  • threadFactory:线程工厂,线程池用于创建线程,内部采用的还是new 一个Thread方法
  • handler:拒绝策略,当线程池满了或队列满了,则出发拒绝策略

常见线程池

  • newSingleThreadExecutor:创建只有单个线程的线程池,适用于需要顺序执行任务的场景
  • newCachedThreadPool:创建一个可无限扩大的线程池,maximumPoolSize无穷大,不会被塞入队列中,适用于负载较轻,执行短期异步任务(可以使任务得到快速执行,因为执行短,可以很快结束,无需CPU频繁切换)
  • newFixedThreadPool:无边界,创建一个固定大小的线程池,即核心与最大线程都为固定,塞入阻塞队列,适用于负载较重,对当前线程数量进行限制。
  • newScheduledThreadPool:适用于执行延时或者周期性任务。

由于single、Fixed队列的长度Integer.MAX_VALUE,都容易堆积大量线程请求;
Scheduled、Cached最大线程数的长度为Integer.MAX_VALUE,容易创建大量线程;
所以从系统安全角度着想还是自己创建线程池较好!!

拒绝策略

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

  • AbortPolicy:抛出异常,阻止系统的正常工作
  • CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
  • DiscardOleddestPolicy:丢弃最老的请求(即当前准备运行的任务),并尝试再次提交任务
  • DiscardPolicy:默默丢弃无法处理的任务,并不予以处理

如何配置线程池

  • CPU密集型的任务

    尽量使用较小的线程池,一般为CPU核心数 + 1 ;因为CPU密集型任务使得CPU使用率高,过多的线程,容易导致CPU过多地切换

  • IO密集型任务

    可以使用较大的线程池,一般为 2 * CPU核心数 ,IO密集型的任务CPU的使用率并不高,可以让CPU在等待任务的时候有其他线程去处理别的任务,充分利用CPU时间

  • 混合型任务

    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
    只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
    因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失