线程池的使用与执行流程

68 阅读8分钟

为什么要使用线程池?

创建普通对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁

而线程池其实就是一个容纳了多个线程的容器,其中的线程可以反复使用,无需反复创建线程而消耗过多资源。

线程池的设计:生产者-消费者模式

线程池和一般意义上的池化资源是不同的。一般意义上的池化资源,都是下面这样,当你需要资源的时候就调用 acquire() 方法来申请资源,用完之后就调用 release() 释放资源。但Java 提供的线程池里面压根就没有申请线程和释放线程的方法。

class XXXPool{
  // 获取池化资源
  XXX acquire() {}
  // 释放池化资源
  void release(XXX x){}
}  

目前业界线程池的设计,普遍采用的都是生产者 - 消费者模式线程池的使用方是生产者,线程池本身是消费者。我们可以看看下面JDK线程池的创建和使用示例来理解线程池的生产者-消费者模式。

线程池的创建示例

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.ThreadFactory;

// 初始化示例
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:服务器资源紧张,让空闲的线程快速释放。
  • workQueue:一个任务的执行时长在100~300ms,业务高峰期8个线程,按照10s超时(已经很高了)。10s钟,8个线程,可以处理10 * 1000ms / 200ms * 8 = 400个任务左右,往上再取一点,512已经很多了。
  • handler:极端情况下,一些任务只能丢弃,保护服务端。
  • pool.allowCoreThreadTimeOut(true):也是为了在可以的时候,让线程释放资源。

线程池的使用示例

// 使用execute方法或submit方法提交任务
public class TestClass {
    public static void main(String[] args) {
        //创建包含4个线程的线程池对象
        ExecutorService threadPool=Executors.newFixedThreadPool(4);
        //创建Runnable线程任务对象
        TaskRunnable task=new TaskRunnable();
        System.out.println("提交自定义线程任务");
        //将任务提交线程池,并开始执行
        threadPool.submit(task);
        //关闭线程池
        threadPool.shutdown();
    }
}
class TaskRunnable implements Runnable{
    public void run() {
        System.out.println("自定义线程任务在执行");
        System.out.println("自定义线程任务执行完毕");
    }
}

JDK中的线程池相关类

  • Executor接口:线程池的抽象接口,只包含一个execute方法。
  • ExecutorService子接口:提供了有关终止线程池和Future返回值的一些方法。
  • AbstractExecutorService抽象类:提供了ExecutorService的一些默认实现。
  • ThreadPoolExecutor类:JDK提供的线程池的实现类。
  • Executors类:线程池工厂类,提供了几种线程池的工厂方法。

线程池实现类ThreadPoolExecutor

构造方法

参数最多的一个构造方法共有7个参数,如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

几个重要参数解释

corePoolSize

  • 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,才会去创建一个线程来执行任务
  • 当线程池中的线程数目达到corePoolSize后,就停止线程创建,转而会把任务放到任务队列当中等待。
  • 调用prestartAllCoreThreads()或者prestartCoreThread()方法,可以在没有任务到来之前就预创建线程。

maxPoolSize

  • 当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。
  • 如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。

keepAliveTime & unit

  • 当线程空闲时间达到keepAliveTime,单位unit时,该线程会退出,直到线程数量等于corePoolSize。

workQueue

任务队列(或工作队列),是一个阻塞队列,用来存储等待执行的任务,一般来说,这里的阻塞队列有以下几种选择:

  • ArrayBlockingQueue;
  • LinkedBlockingQueue;
  • SynchronousQueue;
  • PriorityBlockingQueue

threadFactory

线程工厂,通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。

handler

拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收,可以通过 handler 这个参数来指定拒绝的策略。ThreadPoolExecutor 已经提供了以下 4 种策略:

  • ThreadPoolExecutor.AbortPolicy:默认的拒绝策略。丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列等待最久的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。

allowCoreThreadTimeout

是否允许核心线程空闲退出,默认值为false。如果allowCoreThreadTimeout设置为true,则所有线程均会超时退出,直到线程数量为0。

常用方法

  • void execute(Runnable command):提交任务给线程池执行,任务无返回值

带Future返回值的,可以通过Future.get方法获取任务的返回值,get方法会抛出异常。

  • Future<?> submit(Runnable task):由于Runnable接口没有返回值,所以Future返回值执行get()方法返回值为null,作用只是等待,类似于join
  • Future submit(Runnable task, T result):由于Runnable没有返回值,所以额外提供了一个参数,作为返回值。
  • Future submit(Callable task):提交任务给线程池执行,能够返回执行结果
  • void allowCoreThreadTimeOut(boolean value):是否允许核心线程超时,默认false。
  • shutdown():关闭线程池,等待任务都执行完
  • shutdownNow():关闭线程池,不等待任务执行完,并返回等待执行的任务列表。
  • getTaskCount():线程池已执行和未执行的任务总数
  • getCompletedTaskCount():已完成的任务数量
  • getPoolSize():线程池当前的线程数量
  • getActiveCount():当前线程池中正在执行任务的线程数量

线程池的执行流程

线程池最佳实践及注意事项

  • 【强制】使用ThreadPoolExecutor的构造函数声明线程池,避免使用Executors类的 newFixedThreadPool和newCachedThreadPool。
  • 【强制】 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。即threadFactory参数要构造好。
  • 【建议】建议不同类别的业务用不同的线程池。
  • 【建议】CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
  • 【建议】I/O密集型任务(2N):这种任务应用起来,系统会用大部分的时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是2N。
  • 【建议】workQueue不要使用无界队列,尽量使用有界队列。避免大量任务等待,造成OOM。支持有界的阻塞队列有ArrayBlockingQueue 和 LinkedBlockingQueue。
  • 【建议】如果是资源紧张的应用,使用allowsCoreThreadTimeOut可以提高资源利用率。
  • 【建议】虽然使用线程池有多种异常处理的方式,但在任务代码中,使用try-catch最通用,也能给不同任务的异常处理做精细化。
  • 【建议】对于资源紧张的应用,如果担心线程池资源使用不当,可以利用ThreadPoolExecutor的API实现简单的监控,然后进行分析和优化。
  • 【建议】线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。

线程池工厂类Executors

虽然说不推荐生产环境使用Executors类来创建线程池,但是了解一下Executors是如何创建不同特性的线程池,还是有利于我们加深对线程池使用的理解。

4中不同类型线程池的构造方法

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

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 ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}

4中不同类型线程池的比较

线程池类型CachedThreadPoolFixedThreadPoolSingleThreadExecutorScheduledThreadPool
corePoolSize0nThread(接收参数)1nThread(接收参数)
maximumPoolSizeInteger.MAX_VALUEnThread(接收参数)1Integer.MAX_VALUE
keepAliveTime60L0L0L0
unitTimeUnit.SECONDSTimeUnit.MILLSECONDSTimeUnit.MILLSECONDSTimeUnit.NANOSECONDS
workQueueSynchronousQueueLinkedBlockingQueue(无界阻塞队列)LinkedBlockingQueue(无界阻塞队列)DelayedWorkQueue(一个按超时时间升序排序的队列)
通俗解释当有新任务到来,则插入到 SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程则创 建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。核心线程是固定的,阻塞队列是无限大小。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时, 对于新任务会进入阻塞队列中(无界的阻塞队列)创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
适用执行很多短期异步的小程序,或者负载较轻的服务器。执行长期的任务,性能好很多一个任务一个任务执行的场景周期性执行任务的场景
缺点1、SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。所以SynchronousQueue没有起到队列缓冲的效果。2、允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。