总所周知,创建线程比较损耗资源,频繁创建线程容易造成性能降低。因此需要使用线程池,主要原因是可以复用线程,另外使用线程池也可以比较方便的对线程进行管理。下面,让我们一起来看下在Java中如何使用线程池。
ThreadPoolExecutor
ThreadPoolExecutor是Java线程池的具体实现,首先来看下ThreadPoolExecutor的相关参数。
corePoolSize
核心线程数,怎么理解这个参数呢,重点在于核心两个字。为了节省资源,线程池会回收处于空闲状态的线程,但是线程池不会回收核心线程,即使该线程处于空闲状态。
maximumPoolSize
最大线程数,表示的是线程的容量,也就是线程池最多可以创建多少个线程。
keepAliveTime
线程存活时间,当线程空闲的时间超过存活时间,则这个线程会被回收
workQueue
当提交的任务超过线程池的处理能力,也就是所需的线程超过maximumPoolSize,提交的任务会被放入等待队列中。
threadFactory
线程工厂,线程池通过这个创建线程
RejectedExecutionHandler
拒绝策略,当workQueue的容量也被占满时,处理新提交的任务的策略。Java定义了4种策略
Policy | explain |
---|---|
AbortPolicy | 抛出RejectedExecutionException异常 |
CallerRunsPolicy | 在调用方(caller)的线程中运行该任务 |
DiscardOldestPolicy | 丢弃等待最久的任务,并在次提交新任务 |
DiscardPolicy | 丢弃新提交的任务,不做任何处理 |
如果上述几种策略都不满足需求,我们可以自定义策略,只要实现RejectedExecutionHandler接口即可
小结
前文介绍了创建一个线程池所需的相关参数,为了方便理解,在这里类比一个不是很恰当的例子。
假设有一家医院(线程池),总有有10(maximumPoolSize)个医生,平时都会有5(corePoolSize)个医生值班,当病人较多的时候,其他几位医生也会被叫回医院上班,当病人处理完时,这几位被临时叫回的医生又可以下班了,但是值班的5位医生还是得呆在医院。
如果发生突发情况,医院的10位医生都来不及救治的病人,那么病人只好在候诊室(workQueue)等着,当医生处理完手头上的病人时,会从候诊室叫一位病人过来就诊。但是医院的候诊室也是有限制,当候诊人数达到上限时,医院只好劝退(RejectedExecutionHandler)新来的病人
内置线程池
CachedThreadPool
可以看到corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,说明当任务提交,若没有可用的线程,会立马新建线程。
CachedThreadPool比较适合大量执行时间较短的任务,因为当任务执行时间较长时,空闲的线程较少,新任务出现时只能创建新的线程,而线程池的maximumPoolSize又为Integer.MAX_VALUE,这样会导致线程池创建大量的线程,从而可能出现OOM
FixedThreadPool
FixedThreadPool的corePoolSize=maximumPoolSize,说明线程池的线程数是固定的。当任务数超过线程池容量时,任务将被放入等待队列中。
FixedThreadPool比较适合任务数可预估并且相对不变的情况。
SingleThreadExecutor
SingleThreadExecutor的corePoolSize=maximumPoolSize=1,线程池只存在一个线程,同一时刻只有一个任务被执行,其他任务都将放入等待队列中
小结
前文介绍Java内置的几种线程池,但是在实际应用,不推荐使用这几种线程池,而是推荐使用ThreadPoolExecutor来创建线程池,这是为什么呢?
通过观察上述几种线程池的参数,我们发现他们等待队列的容量是没有上限的,当大量任务堆积在等待队列时,容易导致OOM,因此推荐通过ThreadPoolExecutor来创建线程池,这样线程的创建会相对可控。实际上我们也看到,内置的几种线程池也是通过ThreadPoolExecutor来生成的。
测试
下面我们来运行几个例子,以便加深理解
/**
* @author wbl
* @date 2020-02-04
*/
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
// 核心线程数
int corePoolSize = 5;
// 线程池容量
int maximumPoolSize = 10;
// 线程存活时间,单位秒
long keepAliveTime = 1;
// 等待队列容量
int queueSize = 10;
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(queueSize),
new ThreadPoolExecutor.DiscardPolicy());
// 任务数
int taskCount = 50;
// 完成任务所需时间,单位为ms
int completeTaskTime = 1000;
for (int i = 0; i < taskCount; i++){
poolExecutor.execute(()->{
try {
Thread.sleep(completeTaskTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s complete task",Thread.currentThread().getName()));
});
}
// 关闭线程池
poolExecutor.shutdown();
// 等待线程池中的任务执行完毕
while (!poolExecutor.isTerminated()){
}
// 打印完成的任务数
System.out.println("completed task:" + poolExecutor.getCompletedTaskCount());
}
}
执行上述代码,可以得到如下结果
大家可以思考下,为什么提交的任务是50个,而完成的任务数只有20个呢?
任务的执行为1s,而任务的提交的速度远远快于这个,同时上述线程池的最大容量为10,等待队列的容量为10,也就是意味着当提交的任务超过20个时,新提交的任务将会被丢弃,因此只有20个任务会被执行。
为了验证存在任务被丢弃,我们可以修改拒绝策略
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(queueSize),
new ThreadPoolExecutor.AbortPolicy());
再次运行代码,可以看到程序抛出了RejectedExecutionException
大家也可以思考下,怎么修改线程池的参数也能让所有的任务执行完呢?
其实很简单,我们可以调整线程池的maximumPoolSize,让其大于提交的任务数,也可以调整等待队列的容量,让其超过提交的任务数,还可以减缓任务提交的速度,让其大于任务的执行时间。