阿里巴巴Java手册中,关于线程池:
- 线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
- 使用线程池的好处,是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。
- 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
线程池的好处:
- 可以重用线程,避免线程创建的开销;
- 任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成。
一、JUC线程池详解
Java JUC包中的实现类是ThreadPoolExecutor,继承AbstractExecutorService,实现了ExecutorService。
ThreadPoolExecutor比较重要的两个构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其中:
- corePoolSize : 核心线程数量
- maximumPoolSize : 最大线程数量
- keepAliveTime / unit : 空闲线程存活时间
- workQueue : 任务队列
- threadFactory : 线程工厂
- handler : 拒绝策略
1、corePoolSize,核心线程数量,刚创建一个线程池后,不会创建任何线程。
当有新任务到来时,如果当前线程数量小于corePoolSize,会创建一个新线程执行该任务,即使其他线程是空闲的,也会创建新线程。如果线程数量大于corePoolSize,不会立即创建新线程,而是尝试排队,如果因为队列满了或其他原因不能立即入队,就不会排队,而是检查线程个数是否达到了maximumPoolSize,如果没有达到,继续创建新线程,直到线程数达到maximumPoolSize。
流程图
核心线程:当线程个数小于corePoolSize时的线程。
核心线程默认行为:
- 不会预先创建,只有当有任务时才会创建。
- 不会因为空闲而被终止,keepAliveTime参数不适用核心线程。
改变这些默认行为ThreadPoolExecutor有如下方法:
// 预先创建所有核心线程
public int prestartAllCoreThreads();
// 创建一个核心线程,如果所有核心线程都已经创建,返回false
public boolean prestartCoreThread();
// 参数为true时,允许keepAliveTime适用于核心线程
public void allowCoreThreadTimeOut(boolean value)
2、keepAliveTime,目的是为了释放多余的线程资源。当线程池中线程个数大于corePoolSize时额外空闲线程的存活时间。一个非核心线程,在空闲等待新任务的最长等待时间。0表示所有线程都不会超时终止。
3、workQueue,阻塞队列BlockingQueue:
- LinkedBlockingQueue:基于链表,可以指定最大长度,默认无界
- ArrayBlockingQueue:基于数组,有界
- PriorityBlockingQueue:基于堆,无界阻塞优先级队列
- SynchronousQueue:没有实际存储空间的同步阻塞队列。
对于无界队列,线程个数最多只能达到corePoolSize,达到corePoolSize后,新任务总会排队,maximumPoolSize就没有意义了。
对于SynchronousQueue,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到maximumPoolSize。
4、handler,RejectedExecutionHandler,任务拒绝策略
队列有限,并且maximumPoolSize有限,当队列排满,线程个数也达到maximumPoolSize,此时新任务会触发线程池的任务拒绝策略。
ThreadPoolExecutor实现了4种处理方式:
- ThreadPoolExecutor.AbortPolicy:默认方式,抛出异常
- ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛出异常,也不执行
- ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉,然后新任务入队
- ThreadPoolExecutor.CallerRunPolicy:在任务提交者线程中执行新任务,而不是交给线程池的线程执行。
拒绝策略只有在队列有界,maximumPoolSize有限的情况下才会触发。
如果队列无界,服务不了的任务总是会排队;请求队列可能会消耗非常大的内存,甚至引发OOM;
如果队列有界但maximumPoolSize无限,可能会创建过多的线程,占满CPU和内存,使得任何任务都难以完成。
在任务量非常大的场景中,需要让拒绝策略有机会执行。
5、threadFactory,线程工厂
ThreadPoolExecutor的默认实现是Executors类中的静态内部类DefaultThreadFactory。
创建一个线程,设置默认名称(pool-线程池编号-thread-线程编号),设置daemon属性为false,设置线程优先级为标准默认优先级(5)。
如果要自定义线程属性,可以实现自定义的ThreadFactory。
二、工厂类Executors
虽然不推荐直接使用Executors工厂类创建线程池,但还是要了解一下利弊。
1.newSingleThreadExecutor
只使用一个线程,使用无界队列,线程创建后不会超时,顺序执行所有任务。
适用于需要保证所有任务被顺序执行的场合。
无界队列,如果排队任务过多,可能会消耗过多的内存。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
2.newFixedThreadPool
使用固定数目的线程,使用无界队列,线程创建后不会超时终止。
无界队列,如果排队任务过多,可能会消耗过多的内存。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
3.newCachedThreadPool
核心线程数为0,最大线程数为Integer的最大值,线程空闲时间为60秒,队列为SynchronousQueue。
当新任务提交,正好有空闲线程在等待任务,则空闲线程接受该任务,否则总是创建新线程。对任一空闲线程60s内没有新任务则终止。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
使用场景对比:
- 系统负载很高 - newFixedThreadPool
- newFixedThreadPool,通过队列对新任务排队,保证有足够的资源处理实际任务;
- newCachedThreadPool,为每个任务创建一个线程,导致创建过多的线程,竞争CPU和内存资源;
- 系统负载不太高,单个任务执行时间比较短 - newCachedThreadPool
- newCachedThreadPool,效率可能更高,因为任务可以不经排队,直接交给一个空闲线程或新建线程。
- 系统负载可能极高 - 两者都不是最好的选择,应根据具体情况自定义合适的参数。
- newFixedThreadPool,队列过长
- newCachedThreadPool,线程过多
- CPU密集型任务(计算型任务),一般线程数量为CPU数量的1~2倍,过多线程可能增大上下文切换的开销。
- IO密集型任务,相对比CPU密集型任务,需要多一些线程,根据具体的IO阻塞时长进行考量决定。如tomcat,默认最大线程数为200。
网上的帖子质量参差不齐,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~