Java线程池类ThreadPoolExecutor、ScheduledThreadPoolExecutor及Executors工厂类

Java中的线程池类有两个,分别是:ThreadPoolExecutor和ScheduledThreadPoolExecutor,这两个类都继承自ExecutorService。利用这两个类,可以创建各种不同的Java线程池,为了方便我们创建线程池,Java API提供了Executors工厂类来帮助我们创建各种各样的线程池。下面我们分别介绍一下这三个类。

Java线程池ExecutorService继承树:

在这里插入图片描述

ThreadPoolExecutor

ThreadPoolExecutor构造方法及其作用

在这里插入图片描述

  • corePoolSize:线程池维护线程的最少数量
  • maximumPoolSize:线程池维护线程的最大数量
  • keepAliveTime: 线程池维护线程所允许的空闲时间
  • unit: 线程池维护线程所允许的空闲时间的单位
  • workQueue: 线程池所使用的缓冲队列
  • handler: 线程池对拒绝任务的处理策略

线程数量控制

ThreadPoolExecutor线程池中的线程数量是可变的,其变化范围取决于下面两个变量:

  1. corePoolSize:线程池维护线程的最少数量
  2. maximumPoolSize:线程池维护线程的最大数量

具体线程的分配方式是,当一个任务被添加到线程池:

  1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize任务队列workQueue最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务
  5. 当线程池中的线程数量大于 corePoolSize时,多余的线程在跑完任务后,如果空闲时间超过keepAliveTime,线程将被终止。

这样,线程池可以动态的调整池中的线程数。除了corePoolSize、maximumPoolSize两个变量外,ThreadPoolExecutor构造方法还有几个参数:

  • keepAliveTime: 线程池维护线程所允许的空闲时间
  • unit: 线程池维护线程所允许的空闲时间的单位
  • workQueue: 线程池所使用的缓冲队列
  • handler: 线程池对拒绝任务的处理策略

unit 可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:

  • NANOSECONDS
  • MICROSECONDS
  • MILLISECONDS
  • SECONDS

workQueue :
1- 基于链表的FIFO队列,是无界的,默认大小是 Integer.MAX_VALUE
2-是一个BlockingQueue,默认是LinkedBlockingQueue<Runnable>

当线程池池创建的线程数量大于 corePoolSize 后,新来的任务将会加入到堵塞队列(workQueue)中等待有空闲线程来执行。workQueue的类型为BlockingQueue,通常可以取下面三种类型:

  • ArrayBlockingQueue:基于数组的FIFO队列,是有界的,创建时必须指定大小
  • LinkedBlockingQueue: 基于链表的FIFO队列,是无界的,默认大小是 Integer.MAX_VALUE
  • synchronousQueue:一个比较特殊的队列,虽然它是无界的,但它不会保存任务,每一个新增任务的线程必须等待另一个线程取出任务,也可以把它看成容量为0的队列

如果无法将请求加入队列,则创建新的线程.此时如果创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

排队有三种通用策略:

  • 直接提交工作队列的默认选项SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,并且此時尝试先试图把任务加入队列,但是失败了,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线 程具有增长的可能性。

  • 无界队列使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所 有 corePoolSize
    线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因 此,maximumPoolSize
    的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如, 在 Web
    页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

  • 有界队列当使用有限的 maximumPoolSizes 时,有界队列 (如
    ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型
    池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O
    边 界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU
    使用率较高,但是可能遇到不可接受的调度开销,这样也会降 低吞吐量。

使用直接提交策略,也即SynchronousQueue

首先SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加。在这里不是核心线程便是新创建的线程,但是我们试想一样下,下面的场景。

new ThreadPoolExecutor(
   2, 3, 30, TimeUnit.SECONDS,
   new SynchronousQueue<Runnable>(),
   new RecorderThreadFactory("CookieRecorderPool"),
   new ThreadPoolExecutor.CallerRunsPolicy());

handler 是线程池拒绝处理任务的方式,主要有四种类型:

  • ThreadPoolExecutor.AbortPolicy()(系统默认):会抛出 java.util.concurrent.RejectedExecutionException异常,并且丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.CallerRunsPolicy():由调用线程处理该任务,当抛出 RejectedExecutionException异常时,会调用rejectedExecution方法
  • ThreadPoolExecutor.DiscardOldestPolicy():抛弃旧的任务
  • ThreadPoolExecutor.DiscardPolicy():也是丢弃任务,但是不抛出异常。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutorExecutorService的另一个实现类,ScheduledThreadPoolExecutor 类的功能也主要体现在ScheduledExecutorService 接口上.

java.util.concurrent.ScheduledExecutorService接口继承了ExecutorService,它的最主要的功能就是可以对其中的任务进行调度,比如延迟执行、定时执行等等。

ScheduledExecutorService提供了四个方法:

  1. schedule (Runnable task, long delay, TimeUnit timeunit) 在指定延迟之后运行task,没有办法获知task的执行结果
  2. schedule (Callable task, long delay, TimeUnit timeunit) 在指定延迟之后运行task,不过它接收的是一个Callable实例,此方法会返回一个ScheduleFuture对象,通过ScheduleFuture我们可以取消一个未执行的task,也可以获得这个task的执行结果
  3. scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
  4. scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)

schedule Runnable

//schedule Runnable
ScheduledExecutorService scheduledExecutorService =
    Executors.newScheduledThreadPool(5);

ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
    
    
    public Object call() throws Exception {
    
    
        System.out.println("Executed!");
        return "Called!";
    }
},
5,
TimeUnit.SECONDS);
System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();

schedule Callable

//schedule Callable
ScheduledExecutorService scheduledExecutorService =
    Executors.newScheduledThreadPool(5);

ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
    
    
    public Object call() throws Exception {
    
    
        System.out.println("Executed!");
        return "Called!";
    }
},
5,
TimeUnit.SECONDS);

System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();

scheduleAtFixedRate
这个方法的作用是周期性的调度task执行。task第一次执行的延迟根据initialDelay参数确定,以后每一次执行都间隔period时长。

如果task的执行时间大于定义的period,那么下一个线程将在当前线程完成之后再执行。整个调度保证不会出现一个以上任务同时执行(即单线程串行执行)。

在这里插入图片描述

scheduleWithFixedDelay

scheduleWithFixedDelay的参数和scheduleAtFixedRate参数完全一致,它们的不同之处在于对period调度周期的解释。

在scheduleAtFixedRate中,period指的两个任务开始执行的时间间隔,也就是当前任务的开始执行时间和下个任务的开始执行时间之间的间隔。

而在scheduleWithFixedDelay中,period指的当前任务的结束执行时间到下个任务的开始执行时间。

猜你喜欢

转载自blog.csdn.net/weixin_43944305/article/details/113568062