JDK-In-Action-ThreadPoolExecutor

Java线程池

ThreadPoolExecutor提供了一些参数和Hook方法用于调优和扩展.使用Executor的静态方法可以创建预先配置的几种线程池类型, 例如Cached, Fixed, Single等.

调优指南

core and maximum pool sizes

核心线程数与最大池大小(最大线程数). 这两个参数用于任务提交时动态调整线程池中线程的数量.
如果池中当前线程数量小于核心线程数, 则对每一个提交的任务都创建一个新的线程, 不管其他线程是否空闲.
如果池中当前线程数量大于核心线程数, 且小于最大线程数, 则仅当工作队列满时才创建新的线程.
将核心线程数和最大线程数设置一样, 可以得到一个固定大小的线程池.
将最大线程数量设置为Integer.MAX_VALUE, 可以得到一个无限并发量的线程池.
通常在创建线程池时设置这两个参数, 但是也可以在运行时, 通过set方法动态调整.

按需构造

默认情况下, 即使是核心线程也只是在新任务到达时才被创建和启动, 但是可以使用方法prestartCoreThreadprestartAllCoreThreads动态地覆盖它. 如果使用非空队列构造池, 则可能需要预启动线程.

创建新的线程

使用ThreadFactory创建新线程. 如果没有另外指定, 将使用Executors.defaultThreadFactory, 它将创建所有属于相同ThreadGroup的线程, 并且具有相同的NORM_PRIORITY优先级和非守护线程状态. 通过提供不同的ThreadFactory, 可以更改线程的名称, 线程组, 优先级, 是否守护进程等. 如果ThreadFactory通过在调用newThread返回null来表示创建线程失败, executor将继续执行, 但可能无法执行任何任务.
线程应该拥有modifyThread RuntimePermission. 如果工作线程或使用池的其他线程不拥有此权限, 服务可能会降级:配置更改可能不会及时生效, 关闭池可能使池处于终止但尚未完成的状态

Keep-alive times

如果当前池中有超过corePoolSize的线程, 那么当某个线程的空闲时间超过keepAliveTime时则该线程将被终止. 这提供了一种在池没有被积极使用时减少资源消耗的方法. 如果以后池变得更活跃, 就会构造新的线程.
还可以使用setKeepAliveTime(long, TimeUnit)方法动态更改此参数. 当使用Long.MAX_VALUE, TimeUnit.NANOSECONDS时可以有效地阻止空闲线程在线程池关闭之前终止. 默认情况下, keep-alive策略仅适用于池的当前线程数多于corePoolSize的情况. 但是也可以使用allowCoreThreadTimeOut(boolean)方法将这个超时策略应用到核心线程, 只要keepAliveTime值不为0.

超时处理代码见 getTask()方法:

Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();

Queuing

任何BlockingQueue都可以用来传输和保存提交的任务. 此队列的使用与池大小调整相互作用:

  • 如果运行的线程小于corePoolSize, 则executor总是希望添加新线程而不是排队.
  • 如果正在运行corePoolSize或更多的线程, executor总是希望对请求进行排队, 而不是创建新线程.
  • 如果一个请求不能排队, 那么将创建一个新线程, 除非这个线程池的大小超过maximumPoolSize, 这种情况任务将被拒绝(可以调整拒绝策略).

排队有三种基本策略:

  1. 直接移交 Direct handoffs . 工作队列的一个很好的默认选择是SynchronousQueue, 它将任务传递给线程, 而不会占用线程. 在这里, 如果没有立即可用的线程来运行任务, 则对任务进行排队的尝试将失败, 因此将构造一个新线程. 此策略在处理可能具有内部依赖项的请求集时可以避免锁定. 直接移交通常需要无界的maximumPoolSizes, 以避免拒绝新提交的任务. 反过来, 当任务到达的平均速度比它们被处理的速度还要快时, 就有可能出现线程数的无限增长.

  2. 无界队列 Unbounded queues. 当所有的corePoolSize线程都处于繁忙状态时, 使用无界队列(例如没有预定义容量的LinkedBlockingQueue)将导致新任务在队列中等待. 因此, 创建的线程不会超过corePoolSize. (因此, maximumPoolSize的值没有任何影响. )当每个任务完全独立于其他任务时, 这可能是合适的, 因此任务不会影响其他任务的执行;例如, 在web页面服务器中. 虽然这种类型的队列在平滑短暂的请求突发方面很有用, 但它也承认, 当任务到达的平均速度超过处理速度时, 可能会出现工作队列的无限增长.

  3. 有界队列 Bounded queues. 有界的队列(例如, ArrayBlockingQueue)在使用有限的最大池大小时有助于防止资源耗尽, 但是调优和控制可能更困难. 队列大小和最大池大小可以相互交换:使用大队列和小池可以最小化CPU使用、操作系统资源和上下文切换开销, 但是会导致人为的低吞吐量. 如果任务经常阻塞(例如, 它们受到I/O的限制), 相比于使用更多线程的方式, 系统可能会花费更多的调度时间. 使用小队列通常需要更大的池大小, 这会使CPU更忙, 但可能会遇到无法接受的调度开销, 这也会降低吞吐量.

Rejected tasks

在方法execute(Runnable)中提交的新任务将在executor关闭时被拒绝, 并且executor在最大线程和工作队列容量有界并达到饱和时也会拒绝新的任务. 在这两种情况下, execute方法都会调用RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)方法, RejectedExecutionHandler提供四个预定义的处理策略:

  1. ThreadPoolExecutor.AbortPolicy, 默认策略, 直接抛出一个异常RejectedExecutionException;
  2. ThreadPoolExecutor.CallerRunsPolicy, 调用execute本身的线程将运行任务. 这提供了一个简单的反馈控制机制, 可以降低新任务的提交速度(可能导致系统吞吐量突然降低).
  3. ThreadPoolExecutor.DiscardPolicy, 无法执行的新任务将被丢弃.
  4. ThreadPoolExecutor.DiscardOldestPolicy, 如果executor没有关闭, 则删除工作队列头部的任务, 然后重试执行(可能再次失败, 导致重复执行).
    可以定义和使用其他类型的RejectedExecutionHandler类. 谨慎这样做, 特别是当策略被设计为仅在特定容量或队列下工作时.

钩子方法 Hook methods

该类提供受保护的可覆盖的beforeExecute(Thread, Runnable) and afterExecute(Runnable, Throwable)方法, 这些方法在每个任务执行之前和之后调用. 这些可以用来操作执行环境;例如, 重新初始化ThreadLocals, 收集统计信息或添加日志项. 此外, 可以覆盖terminated方法来执行任何需要在executor完全终止后执行的特殊处理. 如果钩子或回调方法抛出异常, 那么内部工作线程可能会失败并突然终止(completedAbruptly=true).

Queue maintenance

方法getQueue()允许访问工作队列, 以便进行监视和调试. 强烈反对将此方法用于任何其他目的. 提供的两个方法remove(Runnable)purge()可用于在大量排队的任务被取消时有助于"storage reclamation".

Finalization

程序中不再引用并且没有剩余线程的池将自动关闭. 如果您想要确保即使用户忘记调用shutdown也能回收未引用的池, 那么必须通过设置适当的keep-alive时间, 使用一个下界0的核心线程和/或设置allowCoreThreadTimeOut(boolean)来安排未使用的线程最终死亡.

具有暂停/恢复功能的扩展示例

 class PausableThreadPoolExecutor extends ThreadPoolExecutor {
   private boolean isPaused;
   private ReentrantLock pauseLock = new ReentrantLock();
   private Condition unpaused = pauseLock.newCondition();

   public PausableThreadPoolExecutor(...) { super(...); }

   protected void beforeExecute(Thread t, Runnable r) {
     super.beforeExecute(t, r);
     pauseLock.lock();
     try {
       while (isPaused) unpaused.await();
     } catch (InterruptedException ie) {
       t.interrupt();
     } finally {
       pauseLock.unlock();
     }
   }

   public void pause() {
     pauseLock.lock();
     try {
       isPaused = true;
     } finally {
       pauseLock.unlock();
     }
   }

   public void resume() {
     pauseLock.lock();
     try {
       isPaused = false;
       unpaused.signalAll();
     } finally {
       pauseLock.unlock();
     }
   }
 }

理解源码关键方法设计

先概览下调用流程, 然后再看源码注释, 读源码.

简化的调用链路

省略了很多代码便于直观的预览整个流程, 建议对照源码和文档查看.
主线流程: 提交任务->execute(task)->addWorker/reject
addWorker的操作: 启动worker线程t.run()=>runWorker() 进入主循环操作(不断的获取任务运行)

//提交的任务最终会执行该方法
execute(task){
 看线程池配置和当前状态执行
  addWorker() 
 或者
  reject() //拒绝策略
}

addWorker(){
 看线程池配置和当前状态执行
  worker = new Worker()
 然后执行 worker.thread.start()
}

//当`worker.thread.start()`被调用时, run方法被触发
Worker implements Runnable{
 run(){
  runWorker()
 }
}


//工作线程的主循环, 循环获取task执行, 否则退出(线程终止)
runWorker(){
 try{
  //注意:getTask返回null会导致当前的Worker线程终止
  while((task=getTask())!=null){
   //线程池终止, 触发中断
   //或确保线程清除中断设置
   beforeExecute() // 线程池钩子
   task.run() // 执行工作任务
   afterExecute() // 线程池钩子
  }
 }finally{
  processWorkerExit()
 }
}

//线程池状态维护
processWorkerExit(){
 移除死亡的worker
 看配置和线程池状态是否需要 addWorker()
}

//工作任务读取操作
getTask(){
 返回工作队列中的task
 或者返回null(线程池shutdown, 所有worker需要被终止 或者 修改了最大池大小导致当前workerCount大于maxniumPoolSize需要缩减worker的数量)
}

ctl 线程池主要状态变量

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

ctl主要控制池的状态, 它是一个原子的整数包装了两个概念的字段:

  • workerCount 指示有效线程数
  • runState 指示是否运行, 关闭等

workerCount是允许开始但不允许停止的工作者数量. 这个值可能与实际的活动线程数有短暂的不同, 例如当ThreadFactory在被请求时没有创建线程, 或者在退出的线程在终止之前仍在执行记帐时. 用户可见的池大小报告为工作集的当前大小.

runState 提供了主要生命周期控制, 有以下几种状态:

  • RUNNING: 接受新任务, 处理队列中的任务
  • SHUTDOWN: 不接受新任务, 但是处理队列中的任务
  • STOP: 不接受新任务, 不处理队列中的任务, 并且中断正在执行的任务
  • TIDYING: 所有线程都已经被终止, workerCount为0, 过渡到该状态将会允许terminated()钩子方法
  • TERMINATED: terminated()方法执行完成

这些值之间的数字顺序很重要, 以便进行有序的比较. 运行状态随时间单调增加, 但不需要触及每个状态. 转换如下:

  • RUNNING -> SHUTDOWN
    调用shutdown()时, 可能隐含在finalize()中
  • (RUNNING or SHUTDOWN) -> STOP
    调用shutdownNow()
  • SHUTDOWN -> TIDYING
    当队列和池都为空时
  • STOP -> TIDYING
    当池为空时
  • TIDYING -> TERMINATED
    当terminate()钩子方法完成时

当状态为TERMINATED, 处于awaitTermination()等待中的线程将返回

检测从SHUTDOWN过渡到TIDYING并不像你想的那么简单, 因为队列可能成为空后又非空, SHUTDOWN期间也会如此, 但是只有在以下情况才终止, 队列是空的且workerCount为0(有时需要重新检查).

getTask

private Runnable getTask(){}

执行阻塞或超时等待的方式获取任务, 具体取决于当前配置项设置的值.
如果出现以下任一种情况导致Worker必须退出(参见runWorker()), 则返回null.

  • 出现超过了maximumPoolSize的worker(由于调用setMaximumPoolSize导致)
  • 线程池已经 stoped
  • 线程池 shutdown 或 队列为空
  • 当前worker获取任务等待超时 且 超时worker可以在超时前后终止(allowCoreThreadTimeOut || workerCount > corePoolSize) 且 队列非空情况下, 当前worker不是池中最后一个线程
    方法返回null时工作者数量需要减1

每个Worker线程启动后是会一直处于runWorker方法的循环中的, 从而始终调用getTask方法获取下一个运行的任务, 如果没有获取到任务或超时, 则会导致该Worker被终止, 也就是线程死亡.死亡之后可能需要重新创建新的Worker线程, 也可能不需要.
这个方法的关键是:

  • 发生线程池shutdown , 则该方法返回null , 之后导致worker被终止 , 逐步的线程池中所有的worker都会被终止;
  • 发生线程池配置参数调整(核心线程数或者最大线程数等)导致当前正在运行中的worker需要减少 , 这时该方法返回null, 触发当前工作者线程终止;

runWorker

final void runWorker(Worker w){}

主工作程序运行循环. 重复的从队列中获取任务并执行, 同时处理一些其他的问题:

  1. 可能从一个初始任务开始, 否则, 只要线程池在运行, 就需要通过getTask获取一个任务. getTask会因为线程池状态变化或者参数配置返回null从而导致worker退出. 其他退出的情况: 其他由外部代码中抛出的异常导致的退出循环情况下completedAbruptly为真, 这通常会执行processWorkerExit来替换这个线程(若有必要新创建一个Worker).
  2. 在运行任何任务之前, 锁被获取以防止任务执行过程中发生其他池中断, 然后确保这个线程没有中断设置除非线程池stopping.
  3. 每个任务运行之前都有一个对beforeExecute的调用, 这可能会抛出一个异常, 在这种情况下, 不执行任务, 然后使线程死亡(跳出循环, 设置completedAbruptly=true ).
  4. 假设beforeExecute正常完成, 运行这个任务, 收集它抛出的任何异常并发送给afterExecute. 分别处理RuntimeException, Error(规范中保证我们会捕捉到这两个错误)和任意的Throwables. 因为不能在Runnable.run中重新抛出Throwables, 所以在抛出时将它们封装在Error中(线程的UncaughtExceptionHandler). 任何抛出的异常也会保守地导致线程死亡.
  5. task.run完成后, 调用afterExecute, 它也可能抛出一个异常, 这也会导致线程死亡. 根据 JLS 规范14.20, 这个异常即使在task.run也抛出异常时同样有效(见源码:afterExecute在finally代码块中). 异常机制的最终效果是, afterExecute和线程的UncaughtExceptionHandler拥有关于用户代码遇到的任何问题的尽可能准确的信息.

processWorkerExit

private void processWorkerExit(Worker w, boolean completedAbruptly){}

为即将死亡的Worker线程做清理和记账. 仅从工作线程调用. 如果worker是因为异常导致的, 即completedAbruptly被设置为真, 则调整workerCount减1. 此方法从工作集中移除线程, 如果线程因用户任务异常而退出, 或者运行的工作线程小于corePoolSize, 或者队列非空但没有工作线程, 则可能终止线程池或创建新的工作线程.

addWorker

private boolean addWorker(Runnable firstTask, boolean core){}

检查是否可以根据当前池状态和给定的界限(核心或最大值)添加新worker. 如果是, workerCount将相应地进行调整, 如果可能, 将创建并启动一个新Worker, 并运行firstTask作为其第一个任务.
如果池已停止或eligible to shut down, 则此方法返回false. 如果线程工厂在被请求时没有创建一个线程, 那么它也会返回false. 如果线程创建失败, 要么是由于线程工厂返回null, 要么是由于异常(通常是由于thread.start()中的OutOfMemoryError)), 这时将回滚(删除Worker,减少workerCount).

JDK内置的几种线程池类型

固定数量的线程池

new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());

创建一个线程池, 该线程池重用固定数量的线程执行无界队列中的任务. 在任何时候, 最多nThreads线程是活跃处理任务的. 如果在所有线程都处于活动状态时提交额外的任务, 它们将在队列中等待, 直到有一个线程可用为止. 如果任何线程在关闭之前的执行过程中由于失败而终止, 那么如果需要执行后续任务, 则创建一个新线程来替代它.
池中的线程将一直存在, 直到显式关闭.

单线程的线程池

new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));

创建一个线程池, 该线程池使用一个工作线程执行无界队列中的任务. (但是请注意, 如果这个线程在关闭之前的执行过程中由于失败而终止, 那么在需要执行后续任务时, 将创建一个新线程来替代它. )任务保证按顺序执行, 并且在任何给定时间都不会有多个任务处于活动状态. 与其他等价的newFixedThreadPool(1)不同, 返回的执行器保证不可重新配置以使用其他线程(即不能动态修改线程池配置参数).

缓存线程池

new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());

创建一个线程池, 该线程池根据需要创建新线程, 但在以前构造的线程可用时将重用它们. 这些池通常会提高执行许多短期异步任务的程序的性能. 执行调用将重用以前构造的线程(如果可用). 如果没有可用的现有线程, 将创建一个新线程并将其添加到池中. 未活跃超过60秒的线程将被终止并从缓存中删除. 因此, 长时间空闲的池不会消耗任何资源. 注意, 可以使用ThreadPoolExecutor构造函数创建具有相似属性但不同细节(例如超时时间参数)的池.

引用

  • java.util.concurrent.ThreadPoolExecutor Source Code
posted @ 2020-04-30 15:24  onion94  阅读(198)  评论(0编辑  收藏  举报