常用的线程池模式以及不同线程池的使用场景
答案
1. 基础知识
Java中最基础的线程池服务接口和实现类是 ExecutorService
和 ThreadPoolExecutor
,使用方式如下:
ExecutorService service = new ThreadPoolExecutor(
coreSize,maxSize,keepAliveTime,TimeUnit,BlockingQueue);
serivce.execute(new Runnable(){
public void run(){
}
});
coreSize是线程池的基本线程数,也可以理解为最小保留的线程数;
注意 即使设置了coreSize>0, 线程池中最初也没有任何线程(默认),有任务时才会增加新线程,这称为On-demand construction;
如果高峰期任务量比较大,超过了coreSize,那么超过的任务会被放入阻塞队列BlockingQueue缓冲起来,排队等待执行;
maxSize是线程池的最大线程数,当任务非常密集,不仅超过了基本线程数,并且连Queue都放满了(Queue有界时),那么线程池会新建一些线程来救急,新建的线程数+基本线程数不会超过maxSize;
这些新建的线程在任务高峰期过去后,一般没必要保留,让它们结束就好,因此会有keepAliveTime来决定它们在任务结束后会保留多长时间,TimeUnit是时间单位。
ExecutorService
还有一个扩展接口: ScheduledExecutorService
,它对应的实现类是 ScheduledThreadPoolExecutor
,用来执行哪些需要延迟或反复执行的任务,例如将某次任务延迟10秒执行:
ScheduledExecutorService service = new ScheduledThreadPoolExecutor(5);
service.schedule(new Runnable() {
public void run() {
}
}, 10, TimeUnit.SECONDS);
ScheduledThreadPoolExecutor
中的coreSize线程数是由构造方法传入,并且采用了无界队列,多于coreSize的任务都要排队执行,线程数不会增长总是固定的。
2. 线程池常见模式
JDK 为了方便线程池的使用,提供了一系列工厂方法来创建线程池:
1)Executors.newCachedThreadPool()
它的实现其实是:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
其中SynchronousQueue 实现特点是,没有排队功能,要添加新任务,必须等上一个任务被取走。所以整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。
评价 适用于耗时较短,但比较密集的任务
2)Executors.newFixedThreadPool(nThreads)
它的实现其实是:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。
评价 适用于任务量已知,相对耗时的任务
3)Executors.newSingleThreadExecutor()
它的实现其实是:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
整个线程池表现为:线程数固定为1,任务数多于1时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
评价 相当于单线程串行执行任务,区别时如果线程池中的线程因为任务执行失败而终止,还会新建一个线程,保证池的正常工作。
4)Executors.newScheduledThreadPool(corePoolSize)
它的实现其实是:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务
3. 线程池的使用原则
首先达成几个共识:
- 能同时执行的线程,与你的CPU个数和核数有关
- CPU不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用CPU资源,但当你执行IO操作时、远程RPC调用时,包括进行数据库操作时,这时候CPU就闲下来了,你可以利用多线程提高它的利用率。
- 当你要执行的CPU密集型的计算时,更多的线程不能提高效率
- CPU在不同的线程之间切换是有开销的,如果切换的过于频繁,反而会降低效率
一个合理使用线程池的例子: 构建web服务器(例如tomcat)时,web服务的特点是请求任务量大但是处理时间短,如果为每次请求新建一个线程,线程新建的开销甚至超过了任务处理的开销。 利用线程池,就可以让线程实现重用,轮流处理多个请求任务。