线程池基本介绍与使用

我们知道,在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。之后再可以根据压测来调整。

参考资料


fsta
8 声望0 粉丝