线程池
1.概述
原理:
当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果阻塞队列满了,那就创建新的线程执行当前任务;直到线程池中的线程数达到 maxPoolSize,这时再有任务来,只能执行 reject() 处理该任务。
优点:
- 降低资源消耗;
- 提高响应速度;
- 提高线程的可管理性。
缺点:TODO
1.1 四种常用ExecutorService特性
类型 | 核心线程数 | 最大线程数 | KeepAlive时间(存活时间) | 任务队列 | 拒绝策略 |
---|---|---|---|---|---|
newCachedThreadPool(可缓存线程池) | 0 | Integer.MAX_VALUE | 60s | SynchronousQueue | 线程池无限大,当执行第二个任务已经完成,会复用执行第一个任务的线程。 |
newFixedThreadPool(定长线程池) | 指定大小 | 指定大小(与核心线程数相同) | 0 | LinkedBlockingQueue | 线程池大小固定,没有可用的线程的时候,任务会放在队列等待,队列的长度无限制。 |
newSingleThreadExexutor | 1 | 1 | 0 | LinkedBlockingQueue | 单线程化的线程池,适用于业务逻辑上只允许1个线程进行处理的场景,保证所有任务按照指定顺序FIFO(先进先出),LIFO(后进先出),优先级执行。 |
newScheduledThreadPool | 指定大小 | Integer.MAX_VALUE | 0 | DelayedWordQueue | 定长线程池,支持定时及周期性任务执行。 |
1.2 ThreadPoolExecutor
《阿里巴巴 Java 开发手册》中规定线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。而且线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。这样的处理方式能够更加明确线程池的运行规则,规避资源耗尽的风险。
1 | ThreadPoolExecutor(int corePoolSize, |
参数:
corePoolSize:核心线程数,指定了线程池中的线程池数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中;
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量。
核心和最大线程数大小仅在构建时设置,但也可以使用 setCorePoolSize()
和 setMaximumPoolSize()
进行动态更改。
keepAliveTime:当线程池中的空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁。如果线程池在以后会变得更加活跃,则应构建线程或者使用setKeepAliveTime(long, TimeUnit)
方法。
unit:keepAliveTime的单位
workQueue:阻塞队列(用来保存等待被执行的任务)
- ArrayBlockingQueue:基于数组结构的有界任务队列,按照FIFO排序任务。若有新的任务需要执行时,线程会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程数量达到maximumPoolSize,则执行拒绝策略。这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界任务队列的初始容量比较大或者没有达到超负荷状态,线程数将会一直维持在corePoolSize以下,反之,则会以maximumPoolSize为最大线程数上限。
- 没有预定义容量的LinkedBlockingQueue:基于链表结构的无界任务队列,按照FIFO排序任务。使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,当线程数达到corePoolSize后就不会再增加了。使用无界任务队列将导致新任务在队列中等待,从而导致maximumPoolSize的值没有任何作用。当使用这种任务队列模式时,一定要注意任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。这种队列方式可以用于平滑瞬时大量请求。
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于ArrayBlockingQueue。直接握手队列:它将任务交给线程而不需要保留,如果没有线程立即可用来运行它,那么排队任务的尝试将失败,因此构建新的线程,如果达到maximumPoolSize设置的最大值,则根据设置的handler执行拒绝策略。在这种情况下,需要对程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量避免执行拒绝策略。应注意,当任务持续以平均提交速度大于平均处理速度时,会导致线程数量会无限增长问题。
- PriorityBlockingQueue:具有优先级的无界任务队列。优先任务队列:特殊的无界任务队列,无论添加了多少个任务,线程数量都不会超过corePoolSize。其它队列一般是按照FIFO(先进先出)的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。
threadFactory:线程工程,用于创建线程。如果未另行指定,则使用Executors.defaultThreadFactory默认工厂,使其全部位于同一个ThreadGroup中,并具有相同的NORM_PRIORITY优先级和非守护进程状态。通过不同的ThreadFactory可以更改线程的名称,线程组,优先级,守护进程状态等。privilegedThreadFactory:继承自defaultThreadFactory,主要添加了访问权限校验。
handler:拒绝策略,创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但如果出现任务队列已满且线程池创建的线程数达到maximumPoolSize时,这时就需要指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池”超载”的情况。ThreadPoolExecutor自带的拒绝策略如下:
- AbortPolicy:默认策略,丢掉任务直接抛出RejectedExecutionException异常,阻止系统正常工作。
- CallerRunsPolicy:如果线程池的线程池的线程数量达到上限,该策略会把拒绝的任务放在调用者线程当中运行,如果执行程序已关闭,则会丢弃该任务。
- DiscardPolicy:该策略会默默丢弃无法处理的任务,不会抛出任何异常,使用此策略,业务场景中需允许任务的丢失。
- DiscardOldestPolicy:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的。即每次移除队头元素后再尝试入队。
2.使用
1 | public class TestThreadPool { |