写在前面
上次学习了多线程,了解了线程的概念和作用,学习了线程的创建方式、工作模式和一些重要的方法。当我们使用线程中,创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,就会很大程度上影响处理效率,那么此时我们就引入了线程池的概念,即为避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。下面就来学习下线程池的创建和使用方法。
认识线程池之源 ThreadPoolExecutor
线程池的话一定离不开 ThreadPoolExecutor ,在阿里巴巴的《Java 开发手册》中是这样规定线程池的:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
其实我们通过看源码不难发现,Executors的一些底层就是ThreadPoolExecutor,所以我们只要学习ThreadPoolExecutor 的相关知识,就能掌握线程池的使用。
下面我们就来看看ThreadPoolExecutor 的源码
ThreadPoolExecutor有很多构造方法,我们重点看下涉及核心参数较多的构造方法,源码如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
第 1 个参数:corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程;如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。
第 2 个参数:maximumPoolSize 表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize,此值只有在任务比较多,且不能存放在任务队列时,才会用到。
第 3 个参数:keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
第 4 个参数:unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。
第 5 个参数:workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
第 6 个参数:threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程,我们也可以自定义一个线程工厂,通过实现 ThreadFactory 接口来完成,这样就可以自定义线程的名称或线程执行的优先级了。
第 7 个参数:RejectedExecutionHandler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列 workQueue 中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。
下面来看下线程池的工作流程:
说起线程池的工作流程,要从它的执行方法execute()方法看起,源码如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 当前工作的线程数小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 创建新的线程执行此任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 检查线程池是否处于运行状态,如果是则把任务添加到队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再出检查线程池是否处于运行状态,防止在第一次校验通过后线程池关闭
// 如果是非运行状态,则将刚加入队列的任务移除
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果线程池的线程数为 0 时(当 corePoolSize 设置为 0 时会发生)
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 新建线程执行任务
}
// 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
else if (!addWorker(command, false))
// 执行拒绝策略
reject(command);
}
其中 addWorker(Runnable firstTask, boolean core) 方法的参数说明如下:
firstTask,线程应首先运行的任务,如果没有则可以设置为 null;
core,判断是否可以创建线程的阀值(最大值),如果等于 true 则表示使用 corePoolSize 作为阀值,false 则表示使用 maximumPoolSize 作为阀值。
下面我们就来带着一些常见的问题总结学习下线程池
1.ThreadPoolExecutor 的执行方法有几种?它们有什么区别?
ThreadPoolExecutor的执行方法主要有两个,即execute() 和 submit()
execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。
上一组测试代码如下:
public static void main(String[] args) throws InterruptedException, ExecutionException {
// TODO Auto-generated method stub
ThreadPoolExecutor executor =new ThreadPoolExecutor(2,10,10L,TimeUnit.SECONDS,new LinkedBlockingDeque<>(20));
executor.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Hello jbzhang ,welcome use execute()");
}
});
Future<String> future=executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
System.out.println("Hello jbzhang ,welcome use submit()");
return "SUCCESS";
}
});
System.out.println(future.get());
}
submit() 方法可以配合 Futrue 来接收线程执行的返回值。它们的另一个区别是 execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法。
2.什么是线程的拒绝策略?拒绝策略的分类有哪些?我们如何自定义拒绝策略?
当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。
Java 自带的拒绝策略有 4 种:
AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略;
CallerRunsPolicy,把任务交给当前线程来执行;
DiscardPolicy,忽略此任务(最新的任务);
DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)。
自定义拒绝策略只需要新建一个 RejectedExecutionHandler 对象,然后重写它的 rejectedExecution() 方法即可,如下代码所示:
public static void main(String[] args) throws InterruptedException, ExecutionException {
// TODO Auto-generated method stub
ThreadPoolExecutor executor =new ThreadPoolExecutor(1,3,10,TimeUnit.SECONDS,new LinkedBlockingDeque<>(2),new RejectedExecutionHandler(){
//自定义业务处理方法
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// TODO Auto-generated method stub
System.out.println("Hello jbzhang 执行自定义拒绝策略!");
}
});
for(int i=0;i<6;i++){
executor.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
3.ThreadPoolExecutor 能不能实现扩展?如何实现扩展?
ThreadPoolExecutor 的扩展主要是通过重写它的 beforeExecute() 和 afterExecute() 方法实现的,我们可以在扩展方法中添加日志或者实现数据统计,比如统计线程的执行时间等。