线程池原理浅析

  • 一、概述
  • 二、线程池实现原理
  • 2.1 线程池实现原理
  • 2.2 ThreadPoolExecutor的实现方案
  • 1. ThreadPoolExecutor执行execute()方法的示意图如下:
  • 2. JDK1.8 `ThreadPoolExecutor.execute()` 代码如下:
  • 三、线程池的使用
  • 3.1 线程池的创建
  • 3.2 提交任务
  • 3.3 关闭线程池
  • 3.4 合理分配线程池
  • 3.5 线程池的监控


一、概述

Java中使用异步线程处理任务的场景很多,而频繁的创建销毁线程开销很大。因此在开发过程中,合理地使用线程池有以下几点好处。

  1. 降低资源消耗。重复利用已创建的线程可以降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。无限制地创建线程,会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

二、线程池实现原理

2.1 线程池实现原理

线程池的主要处理流程如下图所示。

swift 异步主线程 异步线程池原理_创建线程


从上图中可以看出,当提交一个新任务到线程池时,线程池处理流程如下。

  1. (线程池)判断核心线程池里的线程数是否达到核心线程数。
  1. 如果创建的核心线程数少于设定的核心线程数,则创建一个新的工作线程来执行任务。
  2. 如果核心线程池里的线程都在执行任务,则进入下一步判断流程。
  1. (线程池)判断工作队列是否已经满。
  1. 如果工作队列没有满,则将新提交的任务存储在这个工作队列里。
  2. 如果工作队列满了,则进入下个判断流程。
  1. (线程池)判断线程池的线程是否都处于工作状态。
  1. 如果没有,则创建一个新的工作线程来执行任务。
  2. 如果已经满了,则交给饱和策略来处理这个任务。

2.2 ThreadPoolExecutor的实现方案

1. ThreadPoolExecutor执行execute()方法的示意图如下:

swift 异步主线程 异步线程池原理_阻塞队列_02


ThreadPoolExecutor.execute() 判断逻辑分下面4步。

  1. 当前运行的线程数少于corePoolSize,则创建新线程来执行任务 (注: 创建新线程会获取全局锁)。
  2. 当前运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
  3. 当BlockingQueue无法添加Task时(队列满了),则创建新的线程来处理任务 (注: 创建新线程会获取全局锁)。
  4. 当线程池内的线程数达到maximumPoolSize时(即无法再创建新线程),将执行
    RejectedExecutionHandler.rejectedExecution() 来拒绝任务 (注: ThreadPoolExecutor中默认提供了4中RejectedExecutionHandler策略)。

小结: 由于创建线程会获取全局锁,为了提升性能应该尽量减少全局锁的获取次数。
因此,在 ThreadPoolExecutor.execute() 方法时尽可能地避免获取全局锁,当满足如下条件(当前运行的线程数大于等于corePoolSize,且队列未满的情况下)时,execute()方法都是执行步骤2(不创建线程,因此不需要获取全局锁)。

2. JDK1.8 ThreadPoolExecutor.execute() 代码如下:
public void execute(Runnable command) {
	if (command == null)
	      throw new NullPointerException();
	  /*
	   * Proceed in 3 steps:
	   *
	   * 1. If fewer than corePoolSize threads are running, try to
	   * start a new thread with the given command as its first
	   * task.  The call to addWorker atomically checks runState and
	   * workerCount, and so prevents false alarms that would add
	   * threads when it shouldn't, by returning false.
	   *
	   * 2. If a task can be successfully queued, then we still need
	   * to double-check whether we should have added a thread
	   * (because existing ones died since last checking) or that
	   * the pool shut down since entry into this method. So we
	   * recheck state and if necessary roll back the enqueuing if
	   * stopped, or start a new thread if there are none.
	   *
	   * 3. If we cannot queue task, then we try to add a new
	   * thread.  If it fails, we know we are shut down or saturated
	   * and so reject the task.
	   */
	  int c = ctl.get();
	  // 1.如果线程数小于基本线程数,则创建线程并执行当前任务
	  if (workerCountOf(c) < corePoolSize) {
	      if (addWorker(command, true))
	          return;
	      c = ctl.get();
	  }
	  // 2.如果线程池处于运行中,且线程数大于等于核心线程数或线程创建失败,则将当前任务放到工作队列中。
	  if (isRunning(c) && workQueue.offer(command)) {
	      int recheck = ctl.get();
	      if (! isRunning(recheck) && remove(command))
	          reject(command);
	      else if (workerCountOf(recheck) == 0)
	          addWorker(null, false);
	  }
	  // 3.如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,则创建一个线程执行任务。
	  else if (!addWorker(command, false))
	  	  // 4.如果当前线程数等于最大线程数(即无法再创建线程),则执行拒绝策略。
	      reject(command);
}

三、线程池的使用

3.1 线程池的创建

public ThreadPoolExecutor(int corePoolSize, //核心线程数
                   		  int maximumPoolSize, //最大线程数
                          long keepAliveTime,	//线程空闲状态下,存活id时间
                          TimeUnit unit, //时间单位
                          BlockingQueue<Runnable> workQueue,  //任务队列
                          ThreadFactory threadFactory,  //创建线程的工程类
                          RejectedExecutionHandler handler  //拒绝任务添加的策略
                          )

创建一个线程池时需要输入几个参数,如下。

  1. corePoolSize(线程池的核心线程数): 当提交一个任务到线程池时,如果线程池内线程数小于corePoolSize时,则会创建一个新线程来执行(即时其他核心线程是空闲状态)。等到核心线程数大于等于corePoolSize大小时就不再创建。可以调线程池的prestartAllCoreThreads()方法,提前创建并启动所有核心线程。
  2. maximumPoolSize(线程池最大线程数): 线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。

注: 如果使用了无界的任务队列,maximumPoolSize参数会失效。因为无界队列永远不会满,所以永远不会触发队列满之后新建线程的操作。

  1. keepAliveTime(线程活动保持时间): 线程池的工作线程空闲后,保持存活的时间。所以,
    如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

注: 当线程池内的 allowCoreThreadTimeOut = false时,keepAliveTime只对非核心线程生效;
当线程池内的 allowCoreThreadTimeOut = true时,keepAliveTime对所有线程生效(包括核心线程);

  1. unit(线程活动保持时间的单位): 是一个枚举,表示 keepAliveTime 的单位。
  2. workQueue(任务队列): 保存等待执行任务的阻塞队列。JDK提供以下几个阻塞队列。
  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
  2. LinkedBlockingQueue:基于链表结构的阻塞队列,FIFO,吞吐量通常要高于ArrayBlockingQueue。
  3. SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue。
  4. PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  1. threadFactory: 创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  2. RejectedExecutionHandler(饱和策略): 当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。默认是AbortPolicy,表示无法处理新任务时抛出异常。ThreadPoolExecutor中提供了以下4种策略。
  1. AbortPolicy:直接抛出异常。
  2. CallerRunsPolicy:只用调用者所在线程来运行任务。
  3. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  4. DiscardPolicy:不处理,丢弃掉。
    当然,也可以自定义策略。如记录日志或持久化存储不能处理的任务。

小结: 线程池的工作过程如下:

  1. 线程池刚创建时,里面没有一个线程。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    3. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程处于空闲状态,且超过一定时间(keepAliveTime)时,线程池会判断。
    1. 当allowCoreThreadTimeOut = false,如果当前存活的线程数大于 corePoolSize,它最终会收缩到 corePoolSize 个线程。
    2. 当allowCoreThreadTimeOut = true,如果当前存活的线程数大于 corePoolSize,它最终会收缩到 0个的线程。

3.2 提交任务

线程池提交任务有如下两个方法:execute()submit()

execute()
不带返回值,无法判断任务是否被线程池执行成功。

submit()
带返回值,线程池会返回一个Future类型的对象。

  1. 可以通过future对象判断任务是否执行成功。
  2. 可以通过future.get()来获取返回值。
  1. get() 会阻塞当前线程直到任务完成。
  2. get(long timeout,TimeUnit unit) 会阻塞当前线程指定时间后立即返回,此时任务可能没有执行完。

3.3 关闭线程池

关闭线程池的方法有两个:shutdown()shutdownNow()

同:

原理: 遍历线程池中的工作线程,然后逐个调用线程的 interrupt() 来中断线程,所以无法响应中断的任务
可能永远无法终止。

异:

shutdownNow(): 先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
shutdown(): 只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线
程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。
当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。

应该调用哪一种方法来关闭线程池?

由提交到线程池的任务特性决定。

  1. 通常调用shutdown方法来关闭线程池。
  2. 如果任务不一定要执行完,则可以调用shutdownNow方法。

3.4 合理分配线程池

如何合理地配置线程池,可以从以下几个角度来分析任务特性。

  1. 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  2. 任务的优先级:高、中和低。
  3. 任务的执行时间:长、中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。

  1. CPU密集型任务应配置尽可能小的线程。
  2. IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程。
  3. 混合型任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。

3.5 线程池的监控

对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题。

  1. taskCount:线程池需要执行的任务数量。
  2. completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  3. largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
  4. getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
  5. getActiveCount:获取活动的线程数。通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。