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