Executor定时任务

59 阅读31分钟

Executor定时任务

池化技术

Java线程的创建与销毁需要一定的开销,如果我们为每一个任务创建一个新线程来执行,这些线程的创建与销毁将消耗大量的计算资源。如果当我们需要使用到多线程时再去创建,使用完又去销毁,这样去使用不仅会拉长业务流程,还会增加创建、销毁线程的开销。

于是有了池化技术的思想,将线程提前创建出来,放在一个池子(容器)中进行管理。

当需要使用时,从池子里拿取一个线程来执行任务,执行完毕后再放回池子。

不仅是线程有池化的思想,连接也有池化的思想,也就是连接池。

池化技术不仅能复用资源、提高响应,还方便管理。

==Java线程既是工作单元,也是执行单元。==从JDK1.5开始,把工作单元与执行机制分离开来。工作单元包括Runnable 和 Callable,而执行机制由Executor框架提供。

线程池的好处

降低资源消耗-重用存在的线程,减少对象创建、消亡的开销,性能好

提高响应速度 -可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。当任务到达时,任务可不用等待线程创建就能立即执行

提高线程的可管理性-提供定时执行、定期执行、单线程、并发数控制等功能。

new Thread的弊端

  • 每次new Thread新建对象,性能差
  • 线程缺乏统一管理,可能无限制的新建线程,相互竞争,可能占用过多的系统资源导致死机或者OOM(out of lmemory内存溢出),这种问题的原因不是因为单纯的new一个Thread,而是可能因为程序的bug或者设计上的缺陷导致不断new Thread造成的。
  • 缺少更多功能,如更多执行,定时执行,线程中断。

Executor框架

Executor框架是什么?

可以暂时把Executor看成线程池的抽象,它定义如何去执行任务

==Executor的初衷是将 任务提交 和 任务执行的细节 解耦。==

public interface Executor {
      void execute(Runnable command);
  }

Executor框架成员

ThreadPoolExecutor、ScheduleThreadPoolExecutor、Future接口、Runnable接口、Callable接口、Executors(工具类)

image.png

线程池类图

image.png

image.png

我们最常使用的Executors实现创建线程池使用线程主要是用上述类图中提供的类。在上边的类图中,包含了一个Executor框架,它是一个根据一组执行策略的调用调度执行和控制异步任务的框架,目的是提供一种将任务提交与任务如何运行分离开的机制。包括如下核心类与接口:

  • Executor:运行新任务的简单接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。

    • void execute(Runnable command);  
      
  • ThreadPoolExecutor: 是线程池的核心实现类,用来执行被提交的任务。

  • ExecutorService:扩展了Executor,添加了用来管理执行器生命周期和任务生命周期的方法。

  • Future,FutureTask类:代表异步计算的结果。

  • ScheduleExcutorService:扩展了ExecutorService,支持Future和定期执行任务。

  • ScheduledThreadPoolExecutor:这是对ThreadPoolExecutor扩展,增加了一些调度逻辑,适用于定时或周期性的任务。

  • ForkJoinPool :这是为ForkJoinTask定制的线程池。内部使用 Work-Stealing 算法。主要是将大问题拆解成小问题,分而治之的方式。也有任务间先后顺序的特性 —— 即解决大问题需先解决小问题。

  • Runnable接口,Callable接口 :都可以被ThreadPoolExecutor或ScheduleThreadPoolExecutor执行。

两级调度模型

Executor框架的两级调度模型

在HotSpot VM的线程模型中,Java线程被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当Java线程终止时,这个操作系统线程也会被回收。操作系统会调用所有线程并将他们分配给可用的CPU。

可以将此种模式分为两层,在上层,Java多线程程序通常把应用程序分解为若干任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

两级调度模型的示意图:

image.png

image.png

框架的结构和成员

1.任务

工作任务被分为两种:无返回结果的Runnable和有返回结果的Callable

image.png

2.任务的执行

包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。

Executor框架有两个关键类实现了ExecutorService接口:ThreadPoolExecutor和 ScheduledThreadPoolExecutor

有的同学可能会有疑问,上文Executor框架定义的执行方法不是只允许传入Runnable任务吗?

Callable任务调用哪个方法来执行呢?

Future接口用来定义获取异步任务的结果,它的实现类常是FutureTask

FutureTask实现Runnable的同时,还用字段存储Callable,在其实现Runnable时实际上会去执行Callable任务

线程池在执行Callable任务时,会将使用FutureTask将其封装成Runnable执行(具体源码我们后面再聊),因此Executor的执行方法入参只有Runnable

FutureTask相当于适配器,将Callable转换为Runnable再进行执行

image.png

Executor 定义线程池,而它的重要实现是ThreadPoolExecutor

ThreadPoolExecutor的基础上,还有个做定时的线程池ScheduledThreadPoolExecutor

image.png

3.异步计算的结果

包括Future和实现Future接口的FutureTask类。

Executor框架的类和接口

框架的使用

image.png

  1. 主线程首先要创建实现 Runnable接口或者Callable接口的任务对象。工具类Executors可以把一个Runnable对象封装为一个Callable对象
Executors.callable(Runnale task);
或
Executors.callable(Runnable task, Object resule);

  1. 然后可以把Runnable对象直接交给ExecutorService执行
ExecutorServicel.execute(Runnable command);或者也可以把Runnable对象或Callable对象提交给ExecutorService执行ExecutorService.submit(Runnable task);

如果执行ExecutorService.submit(...),ExecutorService将返回一个实现Future接口的对象(到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable接口,我们也可以创建FutureTask类,然后直接交给ExecutorService执行。  

  1. 最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

ThreadPoolExecutor

核心参数

ThreadPoolExecutor主要有七个重要的参数

  public ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue,
                                ThreadFactory threadFactory,
                                RejectedExecutionHandler handler)
    
   ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue<Runnable>)
    public static ExecutorService newSingleThreadExecutor() {
       return new FinalizableDelegatedExecutorService
           (new ThreadPoolExecutor(1, 1,
                                   0L, TimeUnit.MILLISECONDS,
                                   new LinkedBlockingQueue<Runnable>()));
}

    
    
    
   ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue<Runnable>, RejectedExecutionHandler);


     
   ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue<Runnable>, ThreadFactory)
   ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue<Runnable>, ThreadFactory, RejectedExecutionHandler)

corePoolSize 线程池核心线程数量。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为ture时,核心线程也会被回收。

maximumPoolSize 线程池允许创建的最大线程数。当等待队列满,就会创建非核心线程来处理,核心线程数+非核心线程数最大为maximumPoolSize。

keepAliveTime 线程闲置超时时长,如果线程闲置时间超过该时长,非核心线程就会被回收。TimeUnit时间单位:非核心线程空闲后存活的时间,常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。

workQueue 存放等待执行任务的阻塞队列

image.png

任务出队执行的规则依赖于具体的实现类,任务队列的常用实现类有:

  • ArrayBlockingQueue :一个数组实现的有界阻塞队列,此队列按照FIFO的原则对元素进行排序,支持公平访问队列。
  • LinkedBlockingQueue :一个由链表结构组成的可选有界阻塞队列,如果不指定大小,则使用Integer.MAX_VALUE作为队列大小,按照FIFO的原则对元素进行排序。
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列,默认情况下采用自然顺序排列,也可以指定Comparator。
  • DelayQueue:一个支持延时获取元素的无界阻塞队列,创建元素时可以指定多久以后才能从队列中获取当前元素,常用于缓存系统设计与定时任务调度等。
  • SynchronousQueue:一个不存储元素的阻塞队列。存入操作必须等待获取操作,反之亦然。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,与LinkedBlockingQueue相比多了transfer和tryTranfer方法,该方法在有消费者等待接收元素时会立即将元素传递给消费者。
  • LinkedBlockingDeque:一个由链表结构组成的双端阻塞队列,可以从队列的两端插入和删除元素。

threadFactory线程工厂:规定如何创建线程,可以根据业务不同规定不同的线程组名称,threadFactory可以设置线程名称、线程组、优先级等参数。可以使用Executors工具类中的方法获取,例如Executors.defaultThreadFactory()

RejectedExecutionHandler 拒绝策略:当线程不够用,并且阻塞队列爆满时如何拒绝任务的策略

拒绝策略作用
ThreadPoolExecutor.AbortPolicy 默认策略抛出异常
ThreadPoolExecutor.DiscardPolicy丢弃掉不能执行的新任务,不抛任何异常。
ThreadPoolExecutor.CallerRunsPolicy当任务队列满时使用调用者的线程直接执行该任务。
ThreadPoolExecutor.DiscardOldestPolicy当任务队列满时丢弃阻塞队列头部的任务(即最老的任务),然后添加当前任务。

线程池中除了构造时的核心参数外,还使用内部类Worker来封装线程和任务,并使用HashSet容器workes工作队列存储工作线程worker

实现原理
流程图

为了清晰的理解线程池实现原理,我们先用流程图和总结概述原理,最后来看源码实现

image.png

如果工作线程数量小于核心线程数量,创建线程、加入工作队列、执行任务

如果工作线程数量大于等于核心线程数量并且线程池还在运行则尝试将任务加入阻塞队列

如果任务加入阻塞队列失败(说明阻塞队列已满),并且工作线程小于最大线程数,则创建线程执行

如果阻塞队列已满、并且工作线程数量达到最大线程数量则执行拒绝策略

execute

线程池有两种提交方式execute和submit,其中submit会封装成RunnableFuture最终都来执行execute

      public <T> Future<T> submit(Callable<T> task) {
          if (task == null) throw new NullPointerException();
          RunnableFuture<T> ftask = newTaskFor(task);
          execute(ftask);
          return ftask;
      }

execute中实现线程池的整个运行流程

  public void execute(Runnable command) {
      //任务为空直接抛出空指针异常
      if (command == null)
          throw new NullPointerException();
      //ctl是一个整型原子状态,包含workerCount工作线程数量 和 runState是否运行两个状态
      int c = ctl.get();
      //1.如果工作线程数 小于 核心线程数 addWorker创建工作线程
      if (workerCountOf(c) < corePoolSize) {
          if (addWorker(command, true))
              return;
          c = ctl.get();
      }
      
      // 2.工作线程数 大于等于 核心线程数时
      // 如果 正在运行 尝试将 任务加入队列
      if (isRunning(c) && workQueue.offer(command)) {
          //任务加入队列成功 检查是否运行
          int recheck = ctl.get();
          //不在运行 并且 删除任务成功 执行拒绝策略 否则查看工作线程为0就创建线程
          if (! isRunning(recheck) && remove(command))
              reject(command);
          else if (workerCountOf(recheck) == 0)
              addWorker(null, false);
      }
      // 3.任务加入队列失败,尝试去创建非核心线程,成功则结束
      else if (!addWorker(command, false))
          // 4.失败则执行拒绝策略
          reject(command);
  }
submit

submit()除了可以接收Runnable对象外,还可接收Callable对象作为参数,但最后都是封装为一个FutureTask对象再调用execute()方法去执行。然后submit()会将这个FutureTask对象返回,那么在主线程就可以通过这个FutureTask对象获知任务当前的状态,并可以通过future.get()方法阻塞等待获取任务执行结果。

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);
线程池状态

线程池有如下五种状态

  • RUNNING:运行状态,线程池可以接收新的任务和执行已入队的任务。
  • SHUTDOWN:线程池处不接收新任务,但不影响正在执行的任务,且能处理已入队的任务。全部处理完毕线程全部回收后进入TIDYING状态。
  • STOP:线程池处不接收新任务,移除已经入队的任务且不处理,同时会中断正在执行的任务。线程全部回收后进入TIDYING状态。
  • TIDYING:线程池中所有的任务已终止,线程数为0;线程池变为TIDYING状态时,会执行钩子函数terminated(),默认实现为空。
  • TERMINATED:钩子函数terminated()被执行完成。
Worker

ThreadPoolExecutor对象的成员变量workers保存了当前所有工作线程,每个工作线程都是封装为了一个内部类Worker的对象。

private final HashSet<Worker> workers = new HashSet<Worker>();

当调用execute()方法提交任务时,若线程池还没满,则调用addWorker()方法添加一个线程并启动线程执行Worker()的run方法

public void run() {
  runWorker(this);
}

run()方法中执行了runWorker()方法,在执行完firstTask后,会循环通过getTask()方法从等待队列workQueue中取任务执行,以此实现了线程复用。

addWorker

addWorker用于创建线程加入工作队列并执行任务

第二个参数用来判断是不是创建核心线程,当创建核心线程时为true,创建非核心线程时为false

  private boolean addWorker(Runnable firstTask, boolean core) {
          //方便跳出双层循环
          retry:
          for (;;) {
              int c = ctl.get();
              int rs = runStateOf(c);
  
              // Check if queue empty only if necessary.
              // 检查状态
              if (rs >= SHUTDOWN &&
                  ! (rs == SHUTDOWN &&
                     firstTask == null &&
                     ! workQueue.isEmpty()))
                  return false;
  
              for (;;) {
                  int wc = workerCountOf(c);
                  //工作线程数已满 返回false 
                  if (wc >= CAPACITY ||
                      wc >= (core ? corePoolSize : maximumPoolSize))
                      return false;
                  //CAS自增工作线程数量 成功跳出双重循环
                  if (compareAndIncrementWorkerCount(c))
                      break retry;
                  //CAS失败 重新读取状态 内循环
                  c = ctl.get();  // Re-read ctl
                  if (runStateOf(c) != rs)
                      continue retry;
                  // else CAS failed due to workerCount change; retry inner loop
              }
          }
  
          //来到这里说明已经自增工作线程数量 准备创建线程
          boolean workerStarted = false;
          boolean workerAdded = false;
          Worker w = null;
          try {
              //创建worker 通过线程工厂创建线程
              w = new Worker(firstTask);
              final Thread t = w.thread;
              if (t != null) {
                  //全局锁
                  final ReentrantLock mainLock = this.mainLock;
                  mainLock.lock();
                  try {
                      // Recheck while holding lock.
                      // Back out on ThreadFactory failure or if
                      // shut down before lock acquired.
                      int rs = runStateOf(ctl.get());
  
                      if (rs < SHUTDOWN ||
                          (rs == SHUTDOWN && firstTask == null)) {
                          if (t.isAlive()) // precheck that t is startable
                              throw new IllegalThreadStateException();
                          //添加线程
                          workers.add(w);
                          int s = workers.size();
                          if (s > largestPoolSize)
                              largestPoolSize = s;
                          //标记线程添加完
                          workerAdded = true;
                      }
                  } finally {
                      mainLock.unlock();
                  }
                  //执行线程
                  if (workerAdded) {
                      t.start();
                      workerStarted = true;
                  }
              }
          } finally {
              if (! workerStarted)
                  addWorkerFailed(w);
          }
          return workerStarted;
      }

addWorker中会CAS自增工作线程数量,创建线程再加锁,将线程加入工作队列workes(hashset),解锁后开启该线程去执行任务

runWorker

worker中实现Runnable的是runWorker方法,在启动线程后会不停的执行任务,任务执行完就去获取任务执行

  final void runWorker(Worker w) {
      Thread wt = Thread.currentThread();
      Runnable task = w.firstTask;
      w.firstTask = null;
      w.unlock(); // allow interrupts
      boolean completedAbruptly = true;
      try {
          //循环执行任务 getTask获取任务
          while (task != null || (task = getTask获取任务()) != null) {
              w.lock();
              // If pool is stopping, ensure thread is interrupted;
              // if not, ensure thread is not interrupted.  This
              // requires a recheck in second case to deal with
              // shutdownNow race while clearing interrupt
              if ((runStateAtLeast(ctl.get(), STOP) ||
                   (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                  !wt.isInterrupted())
                  wt.interrupt();
              try {
                  //执行前 钩子方法
                  beforeExecute(wt, task);
                  Throwable thrown = null;
                  try {
                      //执行
                      task.run();
                  } catch (RuntimeException x) {
                      thrown = x; throw x;
                  } catch (Error x) {
                      thrown = x; throw x;
                  } catch (Throwable x) {
                      thrown = x; throw new Error(x);
                  } finally {
                      //执行后钩子方法
                      afterExecute(task, thrown);
                  }
              } finally {
                  task = null;
                  w.completedTasks++;
                  w.unlock();
              }
          }
          completedAbruptly = false;
      } finally {
          processWorkerExit(w, completedAbruptly);
      }
  }

在执行前后预留两个钩子空方法,留给子类来扩展,后文处理线程池异常也会用到。

钩子方法和回调函数的概念

钩子方法(hook method)是回调函数(callback function)的一种。钩子方法是一种软件设计模式,在该模式中,一个类定义了一个模板方法,其中一部分代码是固定的,另一部分代码可以由子类通过实现钩子方法来自定义。// 模板方法模式

钩子方法是一种特殊的回调函数,它允许子类在父类的算法流程中插入自己的逻辑。在钩子方法中,子类可以通过覆盖父类中的虚方法来提供自定义的实现,从而影响父类算法的执行流程。

回调函数则是指在一个函数执行过程中,调用另一个函数来处理某些事件。回调函数通常作为参数传递给被调用的函数,在特定的时刻被调用以完成相应的任务。钩子方法也可以被视为一种回调函数,它们在父类中被调用,以便在算法执行期间触发子类中的相应逻辑。

钩子函数示例:

public class HookDemo {
    public static void main(String[] args) {
        // 注册 JVM 关闭钩子函数
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("JVM 关闭,执行钩子函数");
            // 可以在此处执行特定的操作,例如保存数据、释放资源等等
        }));
        // 程序主逻辑
        System.out.println("程序开始运行");
        // ...
        System.out.println("程序运行结束");
    }
}

在这个例子中,我们通过调用 Runtime.getRuntime().addShutdownHook() 方法来注册一个 JVM 关闭钩子函数。在程序运行期间,如果 JVM 关闭,该钩子函数会被自动调用,我们可以在钩子函数中执行一些额外的操作,例如保存数据、释放资源等等。

使用Java代码定义一个回调函数的详细步骤

回调函数(callback function)允许一个函数将另一个函数作为参数传递,并在需要的时候调用该函数。在Java中,可以使用接口或Lambda表达式实现回调函数。以下是一个简单的回调函数示例:

// 首先,我们需要定义一个接口,该接口定义了回调函数的规范。在这个例子中,我们定义了一个名为Callback的接口,它有一个方法onResult,该方法接受一个整数参数。
public interface Callback {
    void onResult(int result);
}


// 然后,我们创建一个类,该类中包含了我们需要执行的业务逻辑代码。在这个例子中,我们假设这段代码是计算两个整数的和。
public class Calculator {
    public void add(int a, int b, Callback callback) {
        int result = a + b;
        callback.onResult(result);
    }
}

// 我们可以创建一个实现了Callback接口的类,该类中实现了回调函数onResult。
public class LoggingCallback implements Callback {
    @Override
    public void onResult(int result) {
        System.out.println("Result: " + result);
    }
}

// 现在我们可以在Calculator类的add方法中使用回调函数了。我们可以将一个实现了Callback接口的对象作为参数传递给add方法,在方法执行完成后,调用回调函数的onResult方法,并将计算结果传递给回调函数。
public class Calculator {
    public void add(int a, int b, Callback callback) {
        int result = a + b;
        callback.onResult(result);
    }
}

// 最后,我们可以在main方法中调用Calculator类的add方法,并传递一个LoggingCallback实例作为参数。这样,在执行add方法时,LoggingCallback类的onResult方法就会被调用,并输出计算结果。
public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        LoggingCallback callback = new LoggingCallback();
        calculator.add(1, 2, callback);
    }
}

// 程序执行结果:
Result: 3

使用Java代码定义一个钩子函数的详细步骤

钩子函数(hook function)是一种在软件开发中常用的技术,它可以在程序执行到特定的关

// 我们需要定义一个抽象类或接口,用于定义钩子函数的规范。
public abstract class Hook {
    public void before() {}
    public void after() {}
}

// 我们需要编写一个类,该类中包含了我们需要执行的业务逻辑代码。在这个例子中,我们假设这段代码是计算两个整数的和。
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// 我们创建一个继承自Hook抽象类的钩子函数类,并实现before和after方法。
public class LoggingHook extends Hook {
    @Override
    public void before() {
        System.out.println("Before calculation");
    }
    
    @Override
    public void after() {
        System.out.println("After calculation");
    }
}

// 现在我们可以在Calculator类的add方法中使用钩子函数了。我们可以将钩子函数实例作为参数传递给add方法,然后在方法执行之前调用钩子函数的before方法,在方法执行之后调用钩子函数的after方法。
public class Calculator {
    public int add(int a, int b, Hook hook) {
        hook.before();
        int result = a + b;
        hook.after();
        return result;
    }
}

// 最后,我们可以在main方法中调用Calculator类的add方法,并传递一个LoggingHook实例作为参数。这样,在执行add方法时,LoggingHook类的before和after方法就会被调用。
public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        LoggingHook hook = new LoggingHook();
        int result = calculator.add(1, 2, hook);
        System.out.println("Result: " + result);
    }
}

// 当程序执行时,输出如下:

Before calculation
After calculation
Result: 3

回调函数是一种非常重要的编程技术,它可以在很多场景中提供非常有用的功能。以下是一些使用回调函数的场景:

异步操作:在进行异步操作(例如网络请求、文件读取等)时,使用回调函数可以避免阻塞程序,让程序继续执行其他操作。异步操作完成后,回调函数将被调用,程序可以处理操作的结果。// 简单的理解起来就是把异步执行的逻辑封装成一个方法,然后就成了回调函数

事件处理:在事件驱动的编程模型中,回调函数常常用于处理事件。当一个特定事件发生时,回调函数会被调用,以响应事件。

动态编程:使用回调函数可以使程序更加动态。回调函数允许程序在运行时根据需要修改代码,例如在某些情况下执行不同的操作。

可复用性:使用回调函数可以使代码更加模块化和可复用。回调函数的实现可以被多个函数调用,从而提高代码的可复用性。

可扩展性:回调函数也可以用于实现可扩展的程序。通过提供不同的回调函数实现,程序可以在不修改核心代码的情况下增加新的功能。

总之,回调函数是一种强大而灵活的编程技术,可以帮助程序员处理各种不同的编程场景和问题。使用回调函数可以使程序更加动态、灵活和可扩展,从而提高代码的可维护性和可重用性。

配置参数

线程池中是不是越多线程就越好呢?

首先,我们要明白创建线程是有开销的,程序计数器、虚拟机栈、本地方法栈都是线程私有的空间。

并且线程在申请空间时,是通过CAS申请年轻代的Eden区中一块内存(因为可能存在多线程同时申请所以要CAS)。

线程太多可能导致Eden空间被使用太多导致young gc,并且线程上下文切换也需要开销

因此,线程池中线程不是越多越好,行业内分为两种大概方案。

针对CPU密集型,线程池设置最大线程数量为CPU核心数量+1,避免上下文切换,提高吞吐量,多留一个线程兜底

针对IO密集型,线程池设置最大线程数量为2倍CPU核心数量,由于IO需要等待,为了避免CPU空闲就多一些线程

线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间)),其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)。

具体业务场景需要具体分析,然后加上大量测试才能得到最合理的配置

Executor框架通过静态工厂方法提供几种线程池,比如:

Executors.newSingleThreadExecutor()Executors.newFixedThreadPool()Executors.newCachedThreadPool()

但由于业务场景的不同,最好还是自定义线程池;

处理异常

线程池中如果出现异常会怎么样?

Runnable

当我们使用Runnable任务时,出现异常会直接抛出。

         threadPool.execute(() -> {
             int i = 1;
             int j = 0;
             System.out.println(i / j);
         });

面对这种情况,我们可以在Runnable任务中使用try-catch进行捕获

         threadPool.execute(() -> {
             try {
                 int i = 1;
                 int j = 0;
                 System.out.println(i / j);
             } catch (Exception e) {
                 System.out.println(e);
             }
         });

实际操作的话用日志记录哈,不要打印到控制台

Callable

当我们使用Callable任务时,使用submit方法会获取Future

         Future<Integer> future = threadPool.submit(() -> {
             int i = 1;
             int j = 0;
             return i / j;
         });

如果不使用Future.get()去获取返回值,那么异常就不会抛出,这是比较危险的

为什么会出现这样的情况呢?

前文说过执行submit时会将Callable封装成FutureTask执行

在其下Runnable中,在执行Callable任务时,如果出现异常会封装在FutureTask中

     public void run() {
         //...其他略
         try {
             //执行call任务
             result = c.call();
             ran = true;
         } catch (Throwable ex) {
             //出现异常 封装到FutureTask
             result = null;
             ran = false;
             setException(ex);
         }
         //..
     }

等到执行get时,先阻塞、直到完成任务再来判断状态,如果状态不正常则抛出封装的异常

     private V report(int s) throws ExecutionException {
         Object x = outcome;
         if (s == NORMAL)
             return (V)x;
         if (s >= CANCELLED)
             throw new CancellationException();
         throw new ExecutionException((Throwable)x);
     }

因此在处理Callable任务时,可以对任务进行捕获也可以对get进行捕获

         //捕获任务
         Future<?> f = threadPool.submit(() -> {
             try {
                 int i = 1;
                 int j = 0;
                 return i / j;
             } catch (Exception e) {
                 System.out.println(e);
             } finally {
                 return null;
             }
         });
 ​
         //捕获get
         Future<Integer> future = threadPool.submit(() -> {
             int i = 1;
             int j = 0;
             return i / j;
         });
 ​
         try {
             Integer integer = future.get();
         } catch (Exception e) {
             System.out.println(e);
         }

afterExecutor

还记得线程池的runWorker吗?

它在循环中不停的获取阻塞队列中的任务执行,在执行前后预留钩子方法。

继承ThreadPoolExecutor来重写执行后的钩子方法,记录执行完是否发生异常,如果有异常则进行日志记录,作一层兜底方案。

 public class MyThreadPool extends ThreadPoolExecutor {  
     //...
     
     @Override
     protected void afterExecute(Runnable r, Throwable t) {
         //Throwable为空 可能是submit提交 如果runnable为future 则捕获get
         if (Objects.isNull(t) && r instanceof Future<?>) {
             try {
                 Object res = ((Future<?>) r).get();
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
             } catch (ExecutionException e) {
                 t = e;
             }
         }
 ​
         if (Objects.nonNull(t)) {
             System.out.println(Thread.currentThread().getName() + ": " + t.toString());
         }
     }
 }

这样即使使用submit,忘记使用get时,异常也不会“消失”

setUncaughtException

创建线程时,可以设置未捕获异常uncaughtException方法,当线程出现异常未捕获时调用,也可以打印日志作兜底

我们定义我们自己的线程工厂,以业务组group为单位,创建线程(方便出错排查)并设置uncaughtException方法

 public class MyThreadPoolFactory implements ThreadFactory {
 ​
     private AtomicInteger threadNumber = new AtomicInteger(1);
     
     private ThreadGroup group;
 ​
     private String namePrefix = "";
 ​
     public MyThreadPoolFactory(String group) {
         this.group = new ThreadGroup(group);
         namePrefix = group + "-thread-pool-";
     }
 ​
 ​
     @Override
     public Thread newThread(Runnable r) {
         Thread t = new Thread(group, r,
                 namePrefix + threadNumber.getAndIncrement(),
                 0);
         t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
             @Override
             public void uncaughtException(Thread t, Throwable e) {
                 System.out.println(t.getName() + ":" + e);
             }
         });
 ​
         if (t.isDaemon()) {
             t.setDaemon(false);
         }
         if (t.getPriority() != Thread.NORM_PRIORITY) {
             t.setPriority(Thread.NORM_PRIORITY);
         }
         return t;
     }
 }
关闭线程池

线程池状态

线程池有如下五种状态

  • RUNNING:运行状态,线程池可以接收新的任务和执行已入队的任务。
  • SHUTDOWN:线程池处不接收新任务,但不影响正在执行的任务,且能处理已入队的任务。全部处理完毕线程全部回收后进入TIDYING状态。
  • STOP:线程池处不接收新任务,移除已经入队的任务且不处理,同时会中断正在执行的任务。线程全部回收后进入TIDYING状态。
  • TIDYING:线程池中所有的任务已终止,线程数为0;线程池变为TIDYING状态时,会执行钩子函数terminated(),默认实现为空。
  • TERMINATED:钩子函数terminated()被执行完成。

关闭线程池两种方案

  1. shutdown
  2. shutdownNow

它们的原理都是: 遍历工作队列wokers中的线程,逐个中断(调用线程的interrupt方法) 无法响应中断的任务可能永远无法终止。

shutdown 任务会被执行完

  1. 将线程池状态设置为SHUTDOWN
  2. 中断所有未正在执行任务的线程

shutdownNow 任务不一定会执行完

  1. 将线程池状态设置为STOP
  2. 尝试停止所有正在执行或暂停任务的线程
  3. 返回等待执行任务列表

image.png

通常使用shutdown,如果任务不一定要执行完可以使用shutdownNow

SecheduledThreadPoolExecutor

ScheduledThreadPoolExecutorThreadPoolExecutor的基础上提供定时执行的功能

两个定时的方法

scheduleAtFixedRate 以任务开始为周期起点,比如说一个任务执行要0.5s,每隔1s执行,相当于执行完任务过0.5s又开始执行任务

scheduledWithFixedDelay 以任务结束为周期起点,比如说一个任务执行要0.5s,每隔1s执行,相当于执行完任务过1s才开始执行任务

         ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(2);
         //scheduleAtFixedRate 固定频率执行任务 周期起点为任务开始
         scheduledThreadPoolExecutor.scheduleAtFixedRate(()->{
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("scheduleAtFixedRate 周期起点为任务开始");
             //初始延迟:1s  周期:1s
         },1,1, TimeUnit.SECONDS);
 ​
         //scheduledWithFixedDelay 固定延迟执行任务,周期起点为任务结束
         scheduledThreadPoolExecutor.scheduleWithFixedDelay(()->{
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("scheduledWithFixedDelay 周期起点为任务结束 ");
             //初始延迟:1s  周期:1s
         },1,1, TimeUnit.SECONDS);

定时线程池使用延迟队列充当阻塞队列实现的

延迟队列是一个延迟队列,它排序存储定时任务,时间越小越先执行

线程获取任务时,会从延迟队列中获取定时任务,如果时间已到就执行

     public RunnableScheduledFuture<?> take() throws InterruptedException {
             final ReentrantLock lock = this.lock;
             lock.lockInterruptibly();
             try {
                 for (;;) {
                     RunnableScheduledFuture<?> first = queue[0];
                     //没有定时任务 等待
                     if (first == null)
                         available.await();
                     else {
                         //获取延迟时间
                         long delay = first.getDelay(NANOSECONDS);
                         //小于等于0 说明超时,拿出来执行
                         if (delay <= 0)
                             return finishPoll(first);
                         first = null; // don't retain ref while waiting
                         //当前线程是leader则等待对应的延迟时间,再进入循环取出任务执行
                         //不是leader则一直等待,直到被唤醒
                         if (leader != null)
                             available.await();
                         else {
                             Thread thisThread = Thread.currentThread();
                             leader = thisThread;
                             try {
                                 available.awaitNanos(delay);
                             } finally {
                                 if (leader == thisThread)
                                     leader = null;
                             }
                         }
                     }
                 }
             } finally {
                 if (leader == null && queue[0] != null)
                     available.signal();
                 lock.unlock();
             }
         }

这两个定时方法一个以任务开始为周期起点、另一个以任务结束为周期起点。

获取定时任务的流程是相同的,只是它们构建的定时任务中延迟的时间不同。

定时任务使用period 区别,为正数周期起点为任务开始,为负数时周期起点为任务结束。

Executors工具类

Executors工具类中有一些用来创建预定义线程池的方法

FixedThreadPool

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

主要特点如下:

  • 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue。

  • **应用场景:**FixedThreadPool是线程数量固定的线程池,适用于为了满足资源管理的需求,而需要适当限制当前线程数量的情景,适用于负载比较重的服务器。

代码示例:

/**
     *  @description: 超过线程池大小部分,将拒绝
     *  @author sxp
     *  @date 2021/10/21 14:30
     */
    public static void fixedThreadPool() {
        /**
         *  @description: 线程池使用步骤:1、创建线程池
         */
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //线程池要关闭,一般关闭我们放在finally中执行
        try {
            for (int i = 0; i < 10; i++) {
                /**
                 *  @description:线程池使用步骤:2、线程池执行线程
                 */
                executorService.execute(() -> System.out.println(Thread.currentThread().getName() + ":running"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            /**
             *  @description:线程池使用步骤:3、关闭线程池
             */
            executorService.shutdown();
        }
    }

SingleThreadExecutor

  • SingleThreadExecutor: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

主要特点如下:

  • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

  • newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。

  • **应用场景:**SingleThreadExecutor是只有一个线程的线程池,常用于需要让线程顺序执行,并且在任意时间,只能有一个任务被执行,而不能有多个线程同时执行的场景。

    /**
         *  @description: 线程池大小只有一个
         *  @author sxp
         *  @date 2021/10/21 14:11
         */
        public static void singleThreadExecutor() {
            /**
             *  @description: 线程池使用步骤:1、创建线程池
             */
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            //线程池要关闭,一般关闭我们放在finally中执行
            try {
                for (int i = 0; i < 10; i++) {
                    /**
                     *  @description:线程池使用步骤:2、线程池执行线程
                     */
                    final int temp = i;
                    executorService.execute(() -> System.out.println(Thread.currentThread().getName() + ":running"+"第"+temp+"个线程"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                /**
                 *  @description:线程池使用步骤:3、关闭线程池
                 */
                executorService.shutdown();
            }
        }
    
    

CachedThreadPool

  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}


主要特点如下:

  • 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  • newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
/**
     *  @description: 线程池大小根据请求量自动扩张
     *  @author sxp
     *  @date 2021/10/21 14:40
     */
    public static void cachedThreadPool() {
        /**
         *  @description: 线程池使用步骤:1、创建线程池
         *  可以弹性伸缩的线程池
         */
        ExecutorService executorService = Executors.newCachedThreadPool();
        //线程池要关闭,一般关闭我们放在finally中执行
        try {
            for (int i = 0; i < 10; i++) {
                /**
                 *  @description:线程池使用步骤:2、线程池执行线程
                 */
                executorService.execute(() -> System.out.println(Thread.currentThread().getName() + ":running"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            /**
             *  @description:线程池使用步骤:3、关闭线程池
             */
            executorService.shutdown();
        }
    }

  • ScheduledThreadPool :该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
  • ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,它主要用于在给定的延迟之后运行任务或者定期执行任务;
    • ScheduledThreadPoolExecutor会把待调度的任务放到一个DelayQueue队列中
    • 执行步骤:
      • 线程1从DelayQueue中获取到已到期的ScheduledFutureTask(DelayQueue.take())
      • 线程1执行这个ScheduledFutureTask
      • 线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间
      • 线程1把修改time后的ScheduledFutureTask放回到DelayQueue中,等待下次执行

但在《阿里巴巴 Java 开发手册》中并不推荐使用,原因有两点:

这些预定义的线程池允许的任务队列长度或最大线程数为Integer.MAX_VALUE,可能会导致请求堆积或创建大量线程,从而导致OOM 通过 ThreadPoolExecutor 的方式创建可以更加明确线程池的运行规则,规避资源耗尽的风险。

总结

使用池化技术能够节省频繁创建、关闭的开销,提升响应速度,方便管理,常应用于线程池、连接池等

Executor框架将工作任务与执行(线程池)解耦分离,工作任务分为无返回值的Runnable和有返回值的Callable

Executor实际只处理Runnable任务,会将Callable任务封装成FutureTask适配Runnable执行

线程池使用工作队列来管理线程,线程执行完任务会从阻塞队列取任务执行,当非核心线程空闲一定时间后会被关闭

线程池执行时,如果工作队列线程数量小于核心线程数,则创建线程来执行(相当预热)

如果工作队列线程数量大于核心线程数量,并且阻塞队列未满则放入阻塞队列

如果阻塞队列已满,还未达到最大线程数量则创建非核心线程执行任务

如果已达到最大线程数量则使用拒绝策略

配置参数CPU密集型为CPU核数+1;IO密集型为2倍CPU核数;具体配置需要测试

处理异常可以直接捕获任务,Callable可以捕获get,也可以继承线程池实现afterExecutor记录异常,还可以在创建线程时就设置处理未捕获异常方法

处理定时任务的线程池由延迟队列实现,时间越短的定时任务越先执行,线程会从延迟队列中获取定时任务(时间已到的情况),时间未到就等待

参考文章:

blog.csdn.net/m0_37543627… juejin.cn/post/727594… juejin.cn/post/684490… juejin.cn/post/712783… blog.csdn.net/m0_56602092… blog.csdn.net/hchaoh/arti… zhuanlan.zhihu.com/p/559792588

blog.csdn.net/qq_43410878…