线程池
1 为什么要使用线程池
避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目
2 线程池参数
第1个参数: corePoolSize 表示常驻核心线程数
- 如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;
- 如果大于0,即使本地任务执行完毕,核心线程也不会被销毁.
- 这个值的设置非常关键;
- 设置过大会浪费资源;
- 设置过小会导致线程频繁地创建或销毁.
第2个参数: maximumPoolSize 表示线程池能够容纳同时执行的最大线程数
- 从第1处来看,必须>=1.
- 如果待执行的线程数大于此值,需要借助第5个参数的帮助,缓存在队列中.
- 如果maximumPoolSize = corePoolSize,即是固定大小线程池.
第3个参数: keepAliveTime 表示线程池中的线程空闲时间
- 当空闲时间达到keepAliveTime时,线程会被销毁,直到只剩下corePoolSize个线程;
- 避免浪费内存和句柄资源.
- 在默认情况下,当线程池的线程数大于corePoolSize时,keepAliveTime才起作用.
- 但是当ThreadPoolExecutor的allowCoreThreadTimeOut = true时,核心线程超时后也会被回收.
第4个参数: TimeUnit表示时间单位
- keepAliveTime的时间单位通常是TimeUnit.SECONDS.
第5个参数: workQueue 表示缓存队列
- 当请求的线程数大于maximumPoolSize时,线程进入BlockingQueue.
- 后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性;
- 两个锁分别控制元素的添加和获取,是一个生产消费模型队列.
第6个参数: threadFactory 表示线程工厂
- 它用来生产一组相同任务的线程;
- 线程池的命名是通过给这个factory增加组名前缀来实现的.
- 在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的.
第7个参数: handler 表示执行拒绝策略的对象
- 当超过第5个参数workQueue的任务缓存区上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护.
- 友好的拒绝策略可以是如下三种:
- (1)保存到数据库进行削峰填谷;在空闲时再提取出来执行
- (2)转向某个提示页面
- (3)打印日志
3 线程池的线程创建过程
- 当提交一个任务到线程池时,若线程数量<corePoolSize,线程池会创建一个新线程放入works(一个HashSet)中执行任务,即使其他空闲的基本线程能够执行新任务也还是会创建新线程
- 等到需要执行的任务数大于线程池基本大小时就不再创建,会尝试放入等待队列workQueue
- 如果调用线程池的prestartAllCoreThreads(),线程池会提前创建并启动所有核心线程
- 若队列满,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程放入works中执行任务,CashedThreadPool的关键,固定线程数的线程池无效
- 若使用了无界任务队列,这个参数就没什么效果
4 执行 execute() 方法和 submit() 方法的区别是什么
4.1 execute() 方法
用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
4.2 submit() 方法
用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 future 的 get() 方法来获取返回值,get() 方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
5 如何创建线程池
5.1 方式一:通过构造方法实现
5.2 方式二:通过 Executor 框架的工具类 Executors 来实现
- FixedThreadPool
该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor
方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool
该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
5.3 Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
5.4 建议
通过构造方法手动创建ThreadPoolExecutor的方式创建,这样更加明确线程池的运行规则,规避资源耗尽的风险。
6 线程池的拒绝策略(RejectedExecutionHandler)
6.1 AbortPolicy
丢弃任务,抛出 RejectedExecutionException
6.2 CallerRunsPolicy
只用调用者所在线程来运行任务,有反馈机制,使任务提交的速度变慢)。
6.3 DiscardOldestPolicy
若没有发生shutdown,尝试丢弃队列里最近的一个任务,并执行当前任务, 丢弃任务缓存队列中最老的任务,并且尝试重新提交新的任务
6.4 DiscardPolicy
不处理,丢弃掉, 拒绝执行,不抛异常
当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略.如记录日志或持久化存储不能处理的任务
7 线程池的实现原理
7.1 ThreadPoolExecutor执行execute()分4种情况
- 若当前运行的线程少于corePoolSize,则创建新线程来执行任务(执行这一步需要获取全局锁)
- 若运行的线程多于或等于corePoolSize,则将任务加入BlockingQueue
- 若无法将任务加入BlockingQueue,则创建新的线程来处理任务(执行这一步需要获取全局锁)
- 若创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()
采取上述思路,是为了在执行execute()时,尽可能避免获取全局锁
在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁