Java进阶篇--线程池之ScheduledThreadPoolExecutor

目录

ScheduledThreadPoolExecutor简介

构造方法

特有方法

可周期性执行的任务-ScheduledFutureTask

DelayedWorkQueue

什么是DelayedWorkQueue?

为什么要使用DelayedWorkQueue呢?

DelayedWorkQueue的数据结构

ScheduledThreadPoolExecutor执行过程

总结


ScheduledThreadPoolExecutor简介

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor(关于ThreadPoolExecutor可以看这篇文章),并实现了ScheduledExecutorService接口,因此它同时具备了线程池的基本功能和任务调度的功能。ScheduledThreadPoolExecutor可以用来延迟执行任务或者周期性执行任务,相比于传统的Timer类,其功能更加强大和灵活。

在UML图中,可以看到ScheduledThreadPoolExecutor关联了DelayedWorkQueue和ScheduledFutureTask这两个关键的内部类。DelayedWorkQueue实现了BlockingQueue接口,提供了阻塞队列的功能,用于存储延迟执行的任务。而ScheduledFutureTask继承自FutureTask类,表示异步任务的结果。

ScheduledThreadPoolExecutor的构造函数可以指定后台线程的个数,这使得开发者可以更灵活地控制任务的执行。而ScheduledExecutorService接口定义了延时执行任务和周期执行任务的方法,使得任务调度变得更加简单和便捷。

总之,ScheduledThreadPoolExecutor通过继承ThreadPoolExecutor和实现ScheduledExecutorService接口,结合内部的DelayedWorkQueue和ScheduledFutureTask类,为开发者提供了一个功能强大、灵活可控的定时任务执行框架。这使得在实际开发中,我们可以更加高效地处理定时任务相关的需求。

构造方法

对于ScheduledThreadPoolExecutor的构造方法,可以看出它继承自ThreadPoolExecutor,因此在构造方法中调用了ThreadPoolExecutor的不同重载形式。下面我将对其构造方法进行简要解释:

1、第一个构造方法

// 第一个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 调用ThreadPoolExecutor的构造方法,指定核心线程池大小为corePoolSize,
    // 最大线程数为Integer.MAX_VALUE,空闲线程存活时间为0纳秒(即不保留空闲线程),
    // 使用NANOSECONDS作为时间单位,以及默认的DelayedWorkQueue作为工作队列。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

这个构造方法指定了核心线程池大小为corePoolSize,并使用了默认的拒绝策略(将任务添加到工作队列中)。最大线程数设为Integer.MAX_VALUE,保证了理论上这是一个大小无界的线程池。

2、第二个构造方法

// 第二个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    // 同样调用ThreadPoolExecutor的构造方法,指定核心线程池大小、最大线程数、空闲线程存活时间等参数,
    // 但在这里允许通过ThreadFactory来自定义线程的创建方式。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory);
}

这个构造方法除了指定了核心线程池大小和最大线程数外,还允许通过ThreadFactory来自定义线程的创建方式。 

3、第三个构造方法

// 第三个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    // 允许设置拒绝策略,当线程池和工作队列都已满时,会调用指定的拒绝策略来处理新任务。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), handler);
}

这个构造方法允许设置拒绝策略,当线程池和工作队列都已满时,会调用指定的拒绝策略来处理新任务。 

4、第四个构造方法

// 第四个构造方法
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    // 组合了前两个构造方法的功能,既允许设置线程工厂,又允许设置拒绝策略。
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}

这个构造方法则组合了前两个构造方法的功能,既允许设置线程工厂,又允许设置拒绝策略。

总之,ScheduledThreadPoolExecutor的构造方法提供了多种选择,可以根据实际需求来灵活配置线程池的参数,从而满足不同的调度任务需求。

特有方法

ScheduledThreadPoolExecutor 实现了 ScheduledExecutorService 接口,并且提供了以下特有方法用于延时执行和周期性执行异步任务:

  • schedule(Runnable command, long delay, TimeUnit unit):在给定的延时时间后执行任务,返回一个 ScheduledFuture 对象,通过它可以获取任务的执行情况。
  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit):类似于上一个方法,但是接受一个 Callable 对象,可以返回任务的计算结果。
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):按固定的速率执行重复的任务,首次执行需要延时 initialDelay,之后每隔 period 时间执行一次任务。如果任务的执行时间超过了 period,则下一个任务会立即开始执行。
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):与 scheduleAtFixedRate 类似,不同之处在于它是在前一个任务结束后,等待固定的延迟时间后再执行下一个任务。

这些方法使得 ScheduledThreadPoolExecutor 能够灵活地执行延时任务和周期性任务,并允许开发人员根据具体需求来调度异步任务的执行。

可周期性执行的任务-ScheduledFutureTask

ScheduledFutureTask 是 Java 中用于支持周期性执行任务的重要类之一。在 ScheduledThreadPoolExecutor 中,当调用 schedule、scheduleAtFixedRate 和 scheduleWithFixedDelay 方法时,实际上是将提交的任务转换成 ScheduledFutureTask 类的实例。这个转换过程发生在 decorateTask 方法中,其作用是对任务进行装饰和封装,以便进行正确的调度和执行。

在 ScheduledFutureTask 中,重写了 run 方法以支持周期性执行的逻辑。在这个重写的 run 方法中,首先判断当前任务是否是周期性任务。如果不是周期性任务,则直接调用 run 方法来执行任务;如果是周期性任务,则会重新设置下一次执行任务的时间,并将下一次任务放入到延迟队列中,以便在指定的时间再次执行。

因此,ScheduledFutureTask 的主要功能是根据任务的周期性特性,对任务进行进一步封装和调度。对于非周期性任务,它直接执行 run 方法;对于周期性任务,则在每次执行完后重新设置下一次执行的时间,并将下一次任务继续放入到延迟队列中。

总的来说,ScheduledFutureTask 在实现周期性任务调度时起到了关键的作用,能够有效地管理和执行周期性任务。这样的设计使得 ScheduledThreadPoolExecutor 能够将任务和线程进行解耦,实现了任务的延时执行和周期性执行的功能。

一个小例子

import java.util.concurrent.*;

public class main {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1);

        // 创建一个周期性执行的任务
        Runnable periodicTask = new Runnable() {
            private long interval = 1000; // 初始执行间隔为1秒

            @Override
            public void run() {
                System.out.println("在执行定期任务 " + System.currentTimeMillis() + " 带间隔 " + interval);
                if (interval != 3000) {
                    interval += 1000; // 每次执行后增加1秒的执行间隔,直到达到3秒
                    rescheduleTask(this, interval, TimeUnit.MILLISECONDS); // 重新调度任务
                }
            }
        };

        // 使用 schedule 方法提交任务,并获得 ScheduledFuture 实例
        ScheduledFuture<?> scheduledFuture = scheduler.schedule(periodicTask, 1, TimeUnit.SECONDS);

        // 关闭 scheduler
        scheduler.shutdown();
    }

    // 动态重新调度任务的方法
    private static void rescheduleTask(Runnable task, long delay, TimeUnit unit) {
        ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);
        executor.schedule(task, delay, unit);
        executor.shutdown();
    }
}

在这个示例中,我们首先创建了一个 ScheduledThreadPoolExecutor 实例 scheduler,然后定义了一个周期性执行的任务 periodicTask。在任务执行时,我们动态地改变了执行间隔,通过调用 rescheduleTask 方法重新调度任务。rescheduleTask 方法中,我们新建了一个临时的 ScheduledThreadPoolExecutor 实例,用于重新调度任务。

通过这个示例,我们展示了如何在运行时动态地改变周期性任务的执行间隔,并且演示了任务是如何被转换成 ScheduledFutureTask 实例,并且如何进行动态调度的。

DelayedWorkQueue

什么是DelayedWorkQueue?

DelayedWorkQueue 在 ScheduledThreadPoolExecutor 中扮演着重要的角色,它是用来存储需要延迟执行或周期执行的任务的数据结构。DelayedWorkQueue 的实现基于堆的数据结构,类似于 DelayQueue 和 PriorityQueue。

在执行定时任务时,每个任务的执行时间各不相同,因此 DelayedWorkQueue 的工作是按照执行时间的升序排列这些任务,确保执行时间距离当前时间越近的任务排在队列的前面。这样,线程池中的工作线程在获取任务时会优先选择执行时间最近的任务,从而实现了延迟执行和周期执行任务的功能。

DelayedWorkQueue 内部实际上使用了一个数组来存储任务,并通过堆的数据结构对任务按照执行时间进行排序。这种设计使得 ScheduledThreadPoolExecutor 能够高效地管理延迟任务,并按照预定的顺序和时间执行这些任务,从而满足异步任务和周期性任务的执行需求。

综上所述,DelayedWorkQueue 在 ScheduledThreadPoolExecutor 中扮演着关键的角色,通过其基于堆的数据结构和按照执行时间排序的机制,能够确保任务按照预期得到执行,为延迟执行和周期执行任务提供了可靠的支持。

为什么要使用DelayedWorkQueue呢?

使用 DelayedWorkQueue 的主要原因是确保定时任务能够按照预定的执行时间顺利进行。DelayedWorkQueue 本质上是一个优先级队列,它能够保证每次出队的任务都是当前队列中执行时间最靠前的,这正是它在 ScheduledThreadPoolExecutor 中的作用所在。

由于 DelayedWorkQueue 基于堆结构实现,在执行插入和删除操作时的时间复杂度为 O(logN),这使得它能够高效地管理延迟任务,并且保证任务按时执行。通过堆结构的特性,DelayedWorkQueue 能够快速找到最近要执行的任务,并将其置于队列的最前面,从而保证任务能够按时得到执行。

综上所述,DelayedWorkQueue 作为基于堆结构的优先级队列,能够有效地管理延迟任务,并确保任务按照预定的执行时间顺利进行。这种设计使得 ScheduledThreadPoolExecutor 能够高效地调度和执行定时任务,提高系统的可维护性和性能。

DelayedWorkQueue的数据结构

DelayedWorkQueue 的数据结构是基于数组构成的,其中数组元素类型为实现了 RunnableScheduledFuture 接口的类(实际上是 ScheduledFutureTask)。在具体实现中,DelayedWorkQueue 使用一个大小为 16 的数组来存储任务,初始大小定义如下:

// 初始容量为16
private static final int INITIAL_CAPACITY = 16;

// 使用数组作为存储结构,初始大小为INITIAL_CAPACITY
private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY]; 

// 用于在多线程环境下对队列进行加锁操作的ReentrantLock对象
private final ReentrantLock lock = new ReentrantLock(); 

// 追踪队列中实际存储的元素个数
private int size = 0; 

这意味着 DelayedWorkQueue 内部使用一个固定大小的数组来管理任务。当任务需要执行时,会根据任务的延迟时间将其放入数组中适当的位置,以保持任务按照执行时间顺序进行排序。这样,待执行时间越近的任务会被放置在队列的前面,以便最先执行。

总的来说,DelayedWorkQueue 的数据结构是基于数组构成的,通过数组来实现对延迟任务的管理和排序,以确保任务能够按照预定的执行时间顺利进行。

ScheduledThreadPoolExecutor执行过程

ScheduledThreadPoolExecutor 的执行过程如下:

1、任务提交:调用 schedule() 方法提交一个任务,该方法将任务转换成 ScheduledFutureTask 对象。

2、延时执行:在 schedule() 方法中调用 delayedExecute() 方法,将任务放入阻塞队列中进行调度。如果线程池已经关闭,则拒绝任务;否则将任务加入到阻塞队列中。具体源码为:

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
	//将提交的任务转换成ScheduledFutureTask
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null,
                                      triggerTime(delay, unit)));
    //延时执行任务ScheduledFutureTask
	delayedExecute(t);
    return t;
}

3、确保线程启动:在 delayedExecute() 方法中会调用 ensurePrestart() 方法,该方法的主要逻辑是确保至少有一个线程处于启动状态,即使核心线程数为 0。在 ensurePrestart() 方法中会调用 addWorker() 方法来添加新的 Worker 线程。

private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
		//如果当前线程池已经关闭,则拒绝任务
        reject(task);
    else {
		//将任务放入阻塞队列中
        super.getQueue().add(task);
        if (isShutdown() &&
            !canRunInCurrentRunState(task.isPeriodic()) &&
            remove(task))
            task.cancel(false);
        else
			//保证至少有一个线程启动,即使corePoolSize=0
            ensurePrestart();
    }
}

4、Worker 线程执行任务:当 Worker 线程启动时,它会不断地从阻塞队列中获取任务并执行,直到获取的任务为 null,此时线程结束终止。

5、当需要执行周期性任务时,Worker 线程在执行完当前任务后会重新计算下一次任务的执行时间,并将任务重新放入阻塞队列,以便下一次执行。

void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}

通过上述步骤,ScheduledThreadPoolExecutor 能够按照预定的时间执行任务,并且能够确保至少有一个线程处于启动状态,以便执行任务。整个流程涉及任务的转换、延时执行、线程池状态的检查、线程的启动和任务的执行等关键步骤。注意:addWorker方法是ThreadPoolExecutor类中的方法,对ThreadPoolExecutor的源码分析可以看这篇文章,很详细。

总结

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,因此在整体功能上保持一致。线程池的主要职责是创建线程(Worker类),而线程则不断地从阻塞队列中获取新的异步任务,直到队列中没有任务为止。相较于ThreadPoolExecutor,ScheduledThreadPoolExecutor具有延时执行任务和定期执行任务的能力。它重新设计了任务类ScheduleFutureTask,并重写了run方法以实现延时执行和周期性执行任务。此外,它使用了DelayedWorkQueue作为阻塞队列,这是一个可根据优先级排序的队列,采用了堆的底层数据结构,使得与当前时间相比,待执行时间越接近的任务被放置在队列的前面,以便线程优先获取并执行这些任务。

在设计线程池时,无论是ThreadPoolExecutor还是ScheduledThreadPoolExecutor,都将任务、执行者和任务结果进行了解耦。执行者的任务执行机制完全交由Worker类负责,任务提交后首先进入阻塞队列,然后通过addWork方法创建Worker类,并通过runWorker方法启动线程,不断地从阻塞队列中获取异步任务执行,直至队列为空为止。任务指的是实现了Runnable接口和Callable接口的实现类,在ThreadPoolExecutor中会将任务转换成FutureTask类,而在ScheduledThreadPoolExecutor中,为了实现延时执行和周期性执行任务的特性,任务会被转换成ScheduledFutureTask类,该类继承了FutureTask,并重写了run方法。提交任务后,可以通过Future接口的类获取任务结果,在ThreadPoolExecutor中是FutureTask类,而在ScheduledThreadPoolExecutor中是ScheduledFutureTask类。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: comparator是Java中的一个接口,用于比较两个对象的大小。它可以用于对集合中的元素行排序,也可以用于自定义排序规则。实现comparator接口需要重写compare方法,该方法返回一个整数值,表示两个对象的大小关系。如果返回负数,则表示第一个对象小于第二个对象;如果返回正数,则表示第一个对象大于第二个对象;如果返回,则表示两个对象相等。comparator接口可以与Java中的排序算法一起使用,例如Collections.sort()方法。 ### 回答2: jmu-java-04面向对象--02-接口-comparator讲述了Java中的接口以及比较器的使用。接口是一种约束,它规定了某个类必须要实现哪些方法,但不需要具体的实现方式。比较器则是一种接口,它规定了两个对象之间的排序方式。 在Java中,接口的定义方式为interface,其中的方法默认为public abstract形式。定义接口时,需要注意接口只能继承接口,并且可以有常量,但不能有成员变量。另外,接口中所有的方法都没有方法体,必须由实现它的类去具体实现。举例来说,如果我们定义一个接口Animal,可以定义一个方法move(),而实现这个接口的类必须实现move()方法,并且可以自由决定具体的实现方式,如Dog类可以实现为跑步,Bird类可以实现为飞行。 在讨论了接口的使用之后,jmu-java-04面向对象--02-接口-comparator着重介绍了比较器的使用。比较器类似于一个工具箱,可以定义多种比较方式供其他类使用。比较器的核心类是Comparator,其定义的方法为compare(),用于比较两个对象并返回结果(0、1或-1)。比较器可以用于对对象行排序或查找指定的对象。 在使用比较器时,需要实现Comparator接口,并覆盖compare()方法。比如,我们可以定义一个Person类,并在其中实现Comparator接口,然后在compare()方法中指定按照年龄从小到大排序。当我们使用Collections.sort()对Person列表行排序时,就会按照我们定义的比较方式行排序。 总的来说,jmu-java-04面向对象--02-接口-comparator讲述了Java中的接口和比较器的使用,这是Java中优秀的编程方式之一,也是开发者必备的基本知识。掌握了接口和比较器的使用,我们就可以更好地实现面向对象编程,并对Java中的集合框架有更深刻的理解。 ### 回答3: Comparator是Java中一个非常重要的接口,它主要用于定义对象之间的比较规则。在Java中,比较规则是由比较器来实现的。比较器可以用于排序、查找和其他需要比较的场景。 Comparator接口有一个方法compare(Object o1, Object o2),用于比较两个对象的大小。如果o1大于o2,则该方法返回一个正整数;如果o1小于o2,则该方法返回一个负整数;如果o1等于o2,则该方法返回0。 我们可以使用Comparator接口来实现自定义的比较规则。比如,我们可以定义一个Student类,包含姓名和年龄两个属性,然后实现一个比较器,按照年龄从小到大的顺序对Student对象行排序。 可以通过使用Collections.sort()方法对Student对象行排序,提供一个实现Comparator接口的比较器作为参数行排序。 实现一个比较器还可以实现多种排序方式。例如,按照姓名从小到大排序,实现如下: ``` public class NameComparator implements Comparator<Student> { public int compare(Student s1, Student s2) { return s1.getName().compareTo(s2.getName()); } } ``` 在使用时,我们可以将NameComparator对象作为参数传递给sort()方法,行姓名排序。 Comparator接口的使用不仅仅局限于对象的比较排序,还可以用于其他需要比较的场景,比如查找、筛选等。例如,我们可以按照年龄筛选出年龄大于20岁的Student对象,并将它们存储在一个新的List中,实现如下: ``` List<Student> ageGreaterThan20 = students.stream() .filter(s -> s.getAge() > 20) .sorted(new AgeComparator()) .collect(Collectors.toList()); ``` 以上的代码使用了Java 8的新特性,使用流将年龄大于20岁的Student对象筛选出来,并按照年龄行排序,最后存储在一个新的List中。 总之,Comparator是一个非常重要的接口,在Java中有着广泛的应用。掌握Comparator的使用可以帮助我们快速地实现对象比较、排序、筛选等操作,提高我们的编程效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员老李头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值