线程池基本介绍与使用
我们知道,在Java中,创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。
所以Java中提供了线程池,其本质就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
JDK线程池相关类
JDK中提供的有关线程池的相关类及其关系如下图:
- Executor接口:线程池的抽象接口,只包含一个execute方法。
- ExecutorService子接口:提供了有关终止线程池和Future返回值的一些方法。
- AbstractExecutorService抽象类:提供了ExecutorService的一些默认实现。
- ThreadPoolExecutor类:JDK提供的线程池的实现类。
- Executors类:线程池工厂类,提供了几种线程池的工厂方法。
下面主要介绍JDK提供的线程池实现类 - ThreadPoolExecutor类。
ThreadPoolExecutor类
Java 提供的线程池相关的工具类中,最核心的是 ThreadPoolExecutor。
构造方法
ThreadPoolExecutor 的构造函数非常复杂,如下面代码所示,这个最完备的构造函数有 7 个参数。
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
下面一一介绍下参数的含义。
corePoolSize
- 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,才会去创建一个线程来执行任务
- 当线程池中的线程数目达到corePoolSize后,就停止线程创建,转而会把任务放到任务队列当中等待。
- 调用prestartAllCoreThreads()或者prestartCoreThread()方法,可以预创建线程,即在没有任务到来之前就创建corePoolSize个线程或者一个线程
maxPoolSize
- 当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。
- 如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
keepAliveTime & unit
当线程空闲时间达到keepAliveTime,单位unit时,该线程会退出,直到线程数量等于corePoolSize。
workQueue
任务队列,一个阻塞队列,用来存储等待执行的任务,建议workQueue不要使用无界队列,尽量使用有界队列。避免大量任务等待,造成OOM。支持有界的阻塞队列有ArrayBlockingQueue 和 LinkedBlockingQueue。
threadFactory
线程工厂,通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
handler
拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收,可以通过 handler 这个参数来指定拒绝的策略。ThreadPoolExecutor 已经提供了以下 4 种策略:
- ThreadPoolExecutor.AbortPolicy:默认的拒绝策略。丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列等待最久的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。
常用方法
提交任务方法
void execute(Runnable command):提交任务给线程池执行,任务无返回值
Future<?> submit(Runnable task):由于Runnable接口没有返回值,所以Future返回值执行get()方法返回值为null,作用只是等待,类似于join
<T> Future<T> submit(Runnable task, T result):由于Runnable没有返回值,所以额外提供了一个参数,作为返回值。
<T> Future<T> submit(Callable<T> task):提交任务给线程池执行,能够返回执行结果。
其他方法
void allowCoreThreadTimeOut(boolean value):是否允许核心线程超时,默认false。
shutdown():关闭线程池,等待任务都执行完
shutdownNow():关闭线程池,不等待任务执行完,并返回等待执行的任务列表。
getTaskCount():线程池已执行和未执行的任务总数
getCompletedTaskCount():已完成的任务数量
getPoolSize():线程池当前的线程数量
getActiveCount():当前线程池中正在执行任务的线程数量
线程池任务的执行流程
线程池任务的一般执行流程图如下图所示:
线程池初始化示例
下面是一个线程池初始化的示例,仅供参考
// 初始化示例
private static final ThreadPoolExecutor pool;
static {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("po-detail-pool-%d").build();
pool = new ThreadPoolExecutor(
4,
8,
60L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(512),
threadFactory, new ThreadPoolExecutor.AbortPolicy());
pool.allowCoreThreadTimeOut(true);
}
初始化参数含义解释:
- threadFactory:给出带业务语义的线程命名。
- corePoolSize:快速启动4个线程处理该业务,是足够的。
- maximumPoolSize:IO密集型业务,我的服务器是4C8G的,所以4*2=8。
- keepAliveTime:服务器资源紧张,让空闲的线程快速释放。
- pool.allowCoreThreadTimeOut(true):也是为了在可以的时候,让线程释放,释放资源。
- workQueue:一个任务的执行时长在100~300ms,业务高峰期8个线程,按照10s超时(已经很高了)。10s钟,8个线程,可以处理10 1000ms / 200ms 8 = 400个任务左右,往上再取一点,512已经很多了。
- handler:极端情况下,一些任务只能丢弃,保护服务端。
线程池使用注意事项
- 避免使用Executors类创建线程池,会有OOM风险。
- 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。即threadFactory参数要构造好。
- 建议不同类别的业务用不同的线程池,至于线程池的数量,各自计算各自的,然后去做压测。
- workQueue不要使用无界队列,尽量使用有界队列。避免大量任务等待,造成OOM。支持有界的阻塞队列有ArrayBlockingQueue 和 LinkedBlockingQueue。
- 如果是资源紧张的应用,使用allowsCoreThreadTimeOut可以提高资源利用率。
- 虽然使用线程池有多种异常处理的方式,但在任务代码中,使用try-catch最通用,也能给不同任务的异常处理做精细化。
- 线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
- CPU密集型任务,最大线程数初始值可以配置N+1。I/O密集型任务,最大线程数初始值可以配置2N。之后再可以根据压测来调整。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。