为什么要用线程池
假设一个这样的场景:客户端有很高的并发量,为了提高效率服务端必然用到多线程,但是服务器监听到一个客户端的连接就要开启一个新的线程进行相应的逻辑处理吗,显然这样是非常不合理的,频繁的线程创建和销毁会让服务器消耗非常多的资源;为了在时间和空间上寻找平衡,线程池由此而生,那么使用线程池有那些好处:
- 减少线程创建和销毁的次数(上下文切换),每个线程都可以被重复利用
- 根据系统需要的承受能力,调整线程池线程的数量,达到适配的目的
Java的Executor框架继承关系图
1.Executor接口
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
Executor接口是java线程池框架的顶级接口,定义了一个用于执行Runnable的execute方法,它没有实现类只有一个重要子接口ExecutorService
2.ExecutorService接口
package java.util.concurrent;
import java.util.List;
import java.util.Collection;
public interface ExecutorService extends Executor {
/**
* 关闭前先提交有序的任务,不再接受新的任务
*/
void shutdown();
/**
* 语义上是立即关闭,暂停所有等待处理的任务并返回这些任务列表
*/
List<Runnable> shutdownNow();
/**
* 执行器是否关闭
*/
boolean isShutdown();
/**
* 任务是否都已经完成
*/
boolean isTerminated();
/**
*阻塞直到所有的任务已经完成或者超时和任务被中断
*/
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
/**
*提交一个实现callable接口的任务
*/
<T> Future<T> submit(Callable<T> task);
/**
* 提交实现Runnable接口的任务,result是返回结果
*/
<T> Future<T> submit(Runnable task, T result);
/**
* 提交一个实现Runnable接口的任务
*/
Future<?> submit(Runnable task);
/**
* 执行给定的任务,当任务全部完成或者超时,返回任务状态和结果的Future列表
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
/**
* 执行给定的任务,如果某个任务已成功完成(也就是未抛出异常),则返回其结果。
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
/**
* 执行给定的任务,如果在给定的超时期满前某个任务已成功完成(也就是未抛出异常),则返回其结果
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
ExecutorService接口实现了Executor接口,定义了一系列任务提交、执行、状态跟踪、终止等方法
shutdown():完成已经提交的任务后,不在接受新的任务
shutdownNow():暂停所有提交的任务,并拒接新的任务
isTerminated():测试所有提交的任务是否已经完成
submit(task):提交实现Runnable或者Callable接口的任务
isShutdown():判断执行器是否关闭
3.Executors类的静态方法
Executors类的静态方法生成各种类型的ExecutorService线程池实例:
newFixedThreadPool(int nThreads):固定线程池线程的数量,并行执行的线程数量不变,当一个线程执行完任务后并不会销毁,可以被重用执行另一个新的任务
newCachedThreadPool():按需创建新线程,就是有任务时才创建,空闲线程保存60s,当前面创建的线程可用时,则重用它们
SingleThreadExecutor():线程池中只有一个线程,依次执行任务
ScheduledThreadPool():线程池按时间计划来执行任务,允许用户设定执行任务的时间
SingleThreadScheduledExcutor();线程池中只有一个线程,它按规定时间来执行任务
线程池实现细节详解
创建线程池有两种方法:一种是 创建一个 ThreadPoolExecutor类的实例对象,完全自己设定线程池的参数,一种是通过Executors类的静态方法生成各种类型的ExecutorService线程池实例,但是通过源码我们会发现Executors类静态方法里面调用的也是ThreadPoolExecutor的构造方法生成的实例对象如下(实现固定线程数量的线程池):
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
下面我们会对线程池的实现类ThreadPoolExecutor的参数进行说明:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize(线程池的基本大小):当提交一个新的任务给线程池后,会创建一个新的线程来执行任务(当前线程池线程数 < corePoolSize),等到需要执行的任务数大于线程池基本大小大于corePoolSize时就不再创建,而是把任务放进保持的等待队列
maximumPoolSize(线程池最大大小):线程池允许最大线程数。如果阻塞队列满了,并且已经创建的线程数小于最大线程数,则线程池会再创建新的线程执行。因为线程池执行任务时是线程池基本大小满了,后续任务进入阻塞队列,阻塞队列满了,在创建线程。
keepAliveTime(线程活动保持时间):空闲的线程保持活动得时间(好像只有在newCachedThreadPool这个方法的参数中才有意义(不为0))
TimeUnit(线程活动保持时间的单位):
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue(工作队列):用于保存待执行的任务的阻塞队列,有如下几种:
- ArrayBlockingQueue:数组结构的有界阻塞队列,先进先出FIFO
- LinkedBlockingQueue:链表结构的无界阻塞队列。先进先出FIFO排序元素,静态方法Executors.newFixedThreadPool和Executors.newFixedThreadPool.newSingleThreadExecutor使用这个队列
- SynchronousQueue:不存储元素的阻塞队列,就是每次插入操作必须等到另一个线程调用移除操作,静态方法Executors.newCachedThreadPool使用这个方法
threadFactory:设置创建线程的工厂,通过这个工厂可以给线程一些比较有意义的名字
handler:一种饱和策略,当线程池和队列都饱满时拒绝处理任务的策略,默认是AbortPolicy,表示无法处理新的任务时抛出异常;拒绝策略有如下几种:
-
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
-
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
-
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
那么什么时候用ThreadPoolExecutor类来创建线程池,什么时候用Executors的静态方法来创建线程池,如果你对线程池的原理理解的比较深入一点或者无法满足你的要求时可以使用ThreadPoolExecutor类来创建线程池,否则平时都建议使用Executors的静态方法来创建线程池,下面是三种静态方法的介绍:
// 固定线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 单线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 无界线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newFixedThreadPool:它的corePoolSize(核心线程数)和maximumPoolSize(最大线程数)是相等的,都是nThread,也就是说当线程池里面的线程数 < nThread时,向线程池提交一个新的任务会直接创建一个新的线程进行处理,所以当任务多的时候理论上线程池会一直保持饱满的状态,当线程池饱满时提交一个新的任务会把任务保持在队列里面。
newSingleThreadExecutor:可以看到它的corePoolSize(核心线程数)和maximumPoolSize(最大线程数)都是1,因为它是单线程池,因此当线程池中的线程因为异常死亡时,会创建一个新的线程去保持它,当线程饱满时,提交一个新的任务也是保持在队列中
newCachedThreadPool:newCachedThreadPool的corePoolSize设置0,即核心池是空,maxmumPoolSize设置为Integer.MAX_VALUE,即maxmumPool是无界的。keepAliveTime设置60L,当空闲线程等待新任务最长时间是60s,超过60s就终止
最后看一下线程池执行流程图
处理流程大概是这样的:
- 判断线程池线程数是否 <corePoolSize(核心线程数),如果是的话则创建一个新的线程来处理任务。
- 其次如果线程数 > corePoolSize并且队列没有满则存储在等待队列中
- 最后判断线程池是否已满 < maximumPoolSize,如果没满则创建一个新的线程来处理任务,否则使用饱和策略来处理这个任务
总结:提交一个新的任务给线程池优先判断基本线程数(corePoolSize)是否已满,如果没满则创建一个新的线程来处理任务,满了其次判断保持任务的队列是否已满,没满把任务加入保持队列,再次判断最大线程数是否已满(maximumPoolSize),最大线程数没满则创建一个新的线程来处理任务,否则使用饱和策略来处理任务