线程池的作用
线程池主要解决两个问题 :
1、当执行大量异步任务时线程池能够提供较好的性能,在不使用线程池时,每当需要执行异步任务时直接 new 一个线程来运行,而线程的创建和销毁是要开销的,但是线程池里面的线程是可复用的 ,不需要每次执行异步任务时都重新创建和销毁线程。
2、线程池也提供了一种资源限制和管理的手段,比如可以限制线程的个数, 动态新增线程等每个ThreadPoolExecutor也保存了一些基本的统计数据, 比如当前线程 池完成的任务数目等。
线程池核心类ThreadPoolExecutor
ThreadPoolExecutor提供了四个构造方法,最常用的是第一个,只要我们理解了构造方法中每一个参数的意义我们就可以理解这些构造方法的具体用处。
//五个参数的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
//六个参数的构造方法-1
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
//六个参数的构造方法-2
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
//七个参数的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
int corePoolSize
该线程池中核心线程数最大值
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程,核心线程默认情况下会一直存活在线程池中,即使这个核心线程处于闲置状态。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程处于闲置状态的话,超过一定时间(keepAliveTime),也会被销毁掉。
int maximumPoolSize
该线程池中线程总数最大值
线程总数 = 核心线程数 + 非核心线程数。
long keepAliveTime
该线程池中非核心线程闲置超时时长
一个非核心线程,如果处于闲置状态的时长超过这个参数所设定的时长,就会被销毁掉。如果设置allowCoreThreadTimeOut = true,核心线程的闲置时间超过设定时长同样会被销毁。
TimeUnit unit
keepAliveTime的单位,TimeUnit是一个枚举类型,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天
BlockingQueue<Runnable> workQueue
用于保存等待执行的任务的阻塞队列
当线程池中核心线程都处于运行状态且线程数小于corePoolSize就新建核心线程,并处理请求。当核心线程数大于corePoolSize就将请求放入workQueue中,线程池中的空闲线程就去从workQueue中取任务并处理,当workQueue放不下新入的任务时,新建非核心线程入池。
常用的workQueue类型:
SynchronousQueue:
它不会为队列中元素维护存储空间,这个队列接收到任务的时候,会直接提交给线程处理。使用SynchronousQueue的目的就是保证“对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。所以在使用这个队列的时候maximumPoolSize一般指定为一个足够大的数。例如Integer.MAX_VALUE。
LinkedBlockingQueue:
这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize。
ArrayBlockingQueue:
可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误。
DelayQueue:
队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
ThreadFactory threadFactory
创建线程的工厂,一般用不上。
RejectedExecutionHandler handler
饱和策略当队列满并且线程个数达到 maximunPoolSize 后采取的策略,RejectedExecutionHandler提供了四种方式来处理饱和策略。
1、直接丢弃(DiscardPolicy)。
2、丢弃队列中最老的任务(DiscardOldestPolicy)。
3、抛异常(AbortPolicy)。
4、将任务分给调用线程来执行(CallerRunsPolicy)。
新建一个线程池的时候,一般只用5个参数的构造函数。执行线程的方法ThreadPoolExecutor.execute((Runnable)。
ThreadPoolExecutor的线程管理策略
- 池子大小小于corePoolSize就新建线程,并处理请求。
- 当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去从workQueue中取任务并处理。
- 当workQueue放不下新入的任务时,新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize就用RejectedExecutionHandler来做拒绝处理。
- 另外,当池子的线程数大于corePoolSize的时候,多余的线程会等待keepAliveTime长的时间,如果无请求可处理就自行销毁。
执行流程图见下图
下面我们来讨论下Java中四个常用线程池
Java通过Executors提供四种线程池,分别为:
1、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
2、newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool
创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
4、newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
使用场景分析
newSingleThreadExecutor的源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从源码中可以看到,corePoolSize与maximumPoolSize都为1,表示线程池只有一个线程;keepAliveTime为0代表永不过期;workQueue为LinkedBlockingQueue,无界阻塞队列。
由源码可分析出,newSingleThreadExecutor()创建了只有一个线程的线程池,线程存活时间为无限时间;当线程繁忙时就将任务添加到workQueue中。
使用场景:一次执行一个任务,且按顺序执行。
newFixedThreadPool的源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
从源码中可以看到,这线程池跟newSingleThreadExecutor唯一的区别是可以设置线程数量。
由源码课分析出,newFixedThreadPool()创建了一个定长的线程池,线程存活时间为无限时间;当线程繁忙时就将任务添加到workQueue中。
使用场景:执行多个耗时任务
newScheduledThreadPool的源码
public class Executors {
...
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
...
}
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
...
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
...
}
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
...
}
从源码中可以看到,corePoolSize为用户设置,maximumPoolSize为int最大值,线程存活时间为无限长,workQueue为DelayedWorkQueue,可以延时一定时间去执行任务。
由源码可分析出,newScheduledThreadPool()创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构。
使用场景:周期性执行任务
newCachedThreadPool的源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
从源码中可以看到,corePoolSize为0,maximumPoolSize为int最大值,线程存活时间为60s,workQueue为SynchronousQueue这个队列接收到任务的时候,会直接提交给线程处理。
由源码可分析出,newCachedThreadPool()创建一个无限线程数的线程池,线程存活时间60s。
使用场景:执行很多短期的异步任务
备注
一般如果线程池任务队列采用LinkedBlockingQueue队列的话,那么不会拒绝任何任务(因为队列大小没有限制),这种情况下,ThreadPoolExecutor最多仅会按照最小线程数来创建线程,也就是说线程池大小被忽略了。如果线程池任务队列采用
ArrayBlockingQueue队列的话,那么ThreadPoolExecutor将会采取一个非常负责的算法,比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。
即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的
第一个任务,为新来的任务腾出空间。这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果积压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;
这时第二个节流阀—最大线程数就起作用了。