Java线程池的使用

线程池介绍

为什么要使用线程池?

Thread pools address two different problems: they usually provide improved performance when executing large numbers of asynchronous tasks, due to reduced per-task invocation overhead, and they provide a means of bounding and managing the resources, including threads, consumed when executing a collection of tasks. Each ThreadPoolExecutor also maintains some basic statistics, such as the number of completed tasks.

上面这段话摘抄自JDK中对于线程池的描述, 可以看出,线程池指的是一个线程的集合,他主要解决了两个我们在使用时的问题:

  1. 对资源进行了预处理,预先创建了线程,省去了线程创建的时间。
  2. 提供了一种对资源进行绑定和管理的方法,包括在执行一组任务时使用的线程。

一个比较容易理解的描述是,我们在使用线程完成任务时。假设线程创建需要的时间为 t1、执行耗时需要 t2、线程销毁需要 t3,那么我们执行一个线程所需的时间为。
线程执行所需时间 = t1+t2+t3
如果我们使用线程池的话,可以预先在线程池中维持一定数量的线程,这样就可以省去t1t3 的时间,由线程池来替我们管理线程的创建和销毁。这样就可以提高系统的响应速度和吞吐率。

Java中的线程池

Java中的线程池属于Java中的Executor框架中的一部分,是ThreadPoolExecutor 类的一个实例。Java中已经给我们提供了现成的线程池,并且鼓励大家在一般情况下直接使用现成的线程池。

However, programmers are urged to use the more convenient Executors factory methods Executors.newCachedThreadPool() (unbounded thread pool, with automatic thread reclamation), Executors.newFixedThreadPool(int) (fixed size thread pool) and Executors.newSingleThreadExecutor() (single background thread), that preconfigure settings for the most common usage scenarios.

Java中提供的现成的线程池有:

  • Executors.newCachedThreadPool()
  • Executors.newFixedThreadPool(int)
  • Executors.newScheduledThreadPool(int)

    CachedThreadPool

    可缓存线程池,具有以下特点:
  • 线程数无限制
  • 有空闲线程则复用空闲线程,若无空闲线程则新建线程
  • 一定程序减少频繁创建/销毁线程,减少系统开销

该线程池的实现如下:

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

可以看到,CachedThreadPool中使用的队列为 SynchronousQueue ,该队列会将任务直接提交给线程来执行,当没有足够的可用线程时将会报异常。并且该线程池把 maximumPoolSizes 参数设置为最大。

FixedThreadPool

定长线程池,该线程池具有以下特点:

  • 可控制线程最大并发数(同时执行的线程数)
  • 超出的线程会在队列中等待

该线程池的实现方法如下:

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

定长的线程池使用 LinkedBlockingQueue 作为线程池使用的队列,使用该队列的话maximumPoolSizes 不起作用,如果所有的coreThread 都是busy的话,新提交的任务将会阻塞会阻塞。

ScheduledThreadPool

用于执行周期性任务的线程池,特点:

  • 支持定时及周期性任务执行。

该线程池主要有scheduleAtFixedRate()、scheduleWithFixedDelay()、schedule()这几个方法。

自定义线程池

ThreadPoolExecutor的参数

一般情况下,Executors提供的线程池已经能够满足大多数的业务场景,如果想自定义线程池的话需要配置ThreadPoolExecutor的几个属性,主要包括如下几个:

  • int corePoolSize=> 该线程池中核心线程数最大值。 核心线程: 线程池新建线程的时候,如果当前线程总数小于 corePoolSize ,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程。 核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。如果指定 ThreadPoolExecutorallowCoreThreadTimeOut 这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。
  • int maximumPoolSize=> 该线程池中线程总数最大值 。线程总数 = 核心线程数 + 非核心线程数。
  • long keepAliveTime。 该线程池中非核心线程闲置超时时长。 一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉。 如果设置allowCoreThreadTimeOut = true,则会作用于核心线程。
  • TimeUnit unit=>keepAliveTime 的单位,TimeUnit 是一个枚举类型。
  • BlockingQueue<Runnable> workQueue 。 该线程池中的任务队列:维护着等待执行的Runnable 对象 当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。

常用的 workQueue 类型:>

  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
  2. LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
  3. ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
  4. DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。

ThreadPoolExecutor 的策略

上面介绍参数的时候其实已经说到了 ThreadPoolExecutor 执行的策略,这里给总结一下,当一个任务被添加进线程池时:

  • 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  • 线程数量达到了corePoolSize,则将任务移入队列等待
  • 队列已满,新建线程(非核心线程)执行任务
  • 队列已满,总线程数又达到了maximumPoolSize,就会由RejectedExecutionHandler抛出异常