文章目录
- 1. 概要
- 2. 固定速率和固定延时
- 2.1 固定速率
- 2.2 固定延时
- 3. API 解释
- 3.1 schedule
- 3.2 固定延时 - scheduleWithFixedDelay
- 3.2 固定速率 - scheduleWithFixedDelay
- 4. 小结
1. 概要
前三篇文章的地址:
- 定时/延时任务-自己实现一个简单的定时器
- 定时/延时任务-Timer用法
- 定时/延时任务-细说Timer源码
ScheduledThreadPoolExecutor
是 Java
并发包 (java.util.concurrent) 中的一个类,同时也是 ThreadPoolExecutor
的一个子类,这就意味者 ScheduledThreadPoolExecutor
不像 Timer
中使用单个线程去执行任务,ScheduledThreadPoolExecutor
使用了线程池去执行,同时 ScheduledThreadPoolExecutor
也具备了 Timer
中的各种功能。
2. 固定速率和固定延时
2.1 固定速率
固定速率 策略表示任务在固定的时间间隔内重复执行,不管任务的执行时间有多长,如果任务的执行时间超过了时间间隔,那么下一个任务会在当前任务执行完毕之后就会马上开始执行,下面是一个例子:假设我们设置了一个固定速率为 5
的任务,从 0s
开始执行,也就是说这个任务 5s
执行一次:
- 第一次执行: 在
0s
开始执行一次,假设执行时间是3s
- 第二次执行: 在
5s
开始执行第二次,假设执行的时候被阻塞了,执行了8s
- 第三次执行: 在
13s
开始执行第三次,假设执行的时候被阻塞了,执行了3s
- 第四次执行: 在
16s
开始执行第四次,假设执行的时候没有被阻塞 - 第四次执行: 在
20s
开始执行第五次,假设执行的时候没有被阻塞 - …
2.2 固定延时
固定延时 策略表示任务在当前任务执行完成之后,固定延时一段时间再执行下一个任务,下面是一个例子:假设我们设置了一个固定速率为 5
的任务,从 0s
开始执行,也就是说这个任务 5s
执行一次:
- 第一次执行: 在
0s
开始执行一次,假设执行时间是3s
- 第二次执行: 在
8s
开始执行第二次,假设执行的时候被阻塞了,执行了8s
- 第三次执行: 在
21s
开始执行第三次,假设执行的时候被阻塞了,执行了3s
- 第四次执行: 在
29
开始执行第四次,假设执行的时候没有被阻塞 - 第四次执行: 在
34s
开始执行第五次,假设执行的时候没有被阻塞 - …
上面的例子中其实固定速率的执行和 Timer
是一样的,但是固定延时有点不一样,不知道你有没有发现,固定延时下执行的时候每一个任务和前一个任务的时间间隔一定是 任务执行时间 + 延时时间
,其实相比于 Timer
,ScheduledThreadPoolExecutor
的固定延时看起来更像是 “正宗” 一点的固定延时
3. API 解释
3.1 schedule
还是老规矩,先看下 ScheduledThreadPoolExecutor 的几个 API,看看用法是什么样的,首先就是 schedule 方法,这个方法就是普通的延时方法,只执行一次,非周期调度
public class Pra {public static void main(String[] args) {ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(16);Thread thread = new Thread(() -> {try {System.out.println("Thread-Current: " + Thread.currentThread().getName() + ", time = " + getTime());if (Math.random() < 0.5) {System.out.println("sleep: 3s");Thread.sleep(3000);} else {System.out.println("sleep: 8s");Thread.sleep(8000);}} catch (InterruptedException e) {e.printStackTrace();}}, "thread-executor-run");executor.schedule(thread, 0, TimeUnit.SECONDS);}private static String getTime() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");return sdf.format(new Date());}}
执行结果如下所示,延迟 0s
就开始执行任务
ScheduledThreadPoolExecutor
也提供了一个 callable
类型的任务的实现,使用 Callable
的好处就是可以获取任务的实现返回值,如下例子所示:
public class Pra {public static void main(String[] args) {ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(16);// 创建一个 Callable 任务Callable<Integer> task = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {System.out.println("Task is running on thread: " + Thread.currentThread().getName());return 1; // 返回结果}};// 调度 Callable 任务,在 0 秒后执行ScheduledFuture<Integer> future = executor.schedule(task, 0, TimeUnit.SECONDS);try {// 获取任务的结果Integer result = future.get();System.out.println("Task result: " + result);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}private static String getTime() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");return sdf.format(new Date());}}
输出结果如下所示:
3.2 固定延时 - scheduleWithFixedDelay
public class Pra {public static void main(String[] args) {ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(16);Thread thread = new Thread(() -> {try {System.out.println("Thread-Current: " + Thread.currentThread().getName() + ", time = " + getTime());if (Math.random() < 0.5) {System.out.println("sleep: 3s");Thread.sleep(3000);} else {System.out.println("sleep: 8s");Thread.sleep(8000);}} catch (InterruptedException e) {e.printStackTrace();}}, "thread-executor-run");executor.scheduleWithFixedDelay(thread, 0, 5, TimeUnit.SECONDS);}private static String getTime() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");return sdf.format(new Date());}}
输入如下所示:
来分析一下:
- 首先第一次任务
2024-11-24 19:19:36:698
启动,然后执行时间3s
- 第二次任务在
2024-11-24 19:19:44:718
启动,跟第一次任务相差8s
,延时 =5s + 3s = 8s
- 第三次任务在
2024-11-24 19:19:52:741
启动,跟第二次任务相差8s
,延时 =5s + 3s = 8s
- 第四次任务在
2024-11-24 19:20:05:760
启动,跟第三次任务相差13s
,延时 =5s + 8s = 13s
- 第五次任务在
2024-11-24 19:20:13:773
启动,跟第四次任务相差8s
,延时 =5s + 3s = 8s
- 第六次任务在
2024-11-24 19:20:26:787
启动,跟第五次任务相差13s
,延时 =5s + 8s = 13s
- …
可以看到 scheduleWithFixedDelay
的延时是相对于当前时间 + 延时时间的,跟 Timer
的固定延时任务有点不同
3.2 固定速率 - scheduleWithFixedDelay
public class Pra {public static void main(String[] args) {ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(16);Thread thread = new Thread(() -> {try {System.out.println("Thread-Current: " + Thread.currentThread().getName() + ", time = " + getTime());if (Math.random() < 0.5) {System.out.println("sleep: 3s");Thread.sleep(3000);} else {System.out.println("sleep: 8s");Thread.sleep(8000);}} catch (InterruptedException e) {e.printStackTrace();}}, "thread-executor-run");executor.scheduleAtFixedRate(thread, 0, 5, TimeUnit.SECONDS);}private static String getTime() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");return sdf.format(new Date());}}
输出结果如下:
来分析下上面的执行流程,需要注意的是,ScheduledThreadPoolExecutor
的固定速率任务会在当前任务执行完之后再添加下一次要执行的任务
- 首先第一次任务
2024-11-24 16:23:25:762
启动,然后执行时间8s
,执行完之后会添加一个2024-11-24 16:23:30 s
执行的任务,当前时间2024-11-24 16:23:33 s
- 第二次任务在
2024-11-24 16:23:33:770
启动,因为2024-11-24 16:23:33:770
超过了2024-11-24 16:23:30 s
这个时间点, 所以立刻执行,执行时间3s
,执行完之后往队列里面添加一个2024-11-24 16:23:35 s
执行的任务,当前时间2024-11-24 16:23:36 s
- 第二次任务在
2024-11-24 16:23:36:784
启动,因为2024-11-24 16:23:36:784
超过了2024-11-24 16:23:35 s
这个时间点, 所以立刻执行,执行时间3s
,执行完之后往队列里面添加一个2024-11-24 16:23:40 s
执行的任务,当前时间2024-11-24 16:23:39 s
- 第四次任务在
2024-11-24 16:23:40:739
启动,因为第三个任务执行完之后时间是2024-11-24 16:23:39 s
,还没有到第四个任务执行的时间点,这时候就等待,等到2024-11-24 16:23:40:739
,执行时间3s
,执行完之后往队列里面添加一个2024-11-24 16:23:45 s
执行的任务,执行完的时间当前是2024-11-24 16:23:42 s
- 第五次任务在
2024-11-24 16:23:45:751
启动,因为第四个任务执行完之后时间是2024-11-24 16:23:42 s
,还没有到第五个任务执行的时间点,这时候就等待,等到2024-11-24 16:23:45:751
,执行时间8s
,执行完之后往队列里面添加一个2024-11-24 16:23:50 s
执行的任务,执行完的时间当前是2024-11-24 16:23:53 s
- 第六次任务在
2024-11-24 16:23:53:763
启动,因为第六个任务执行时间是2024-11-24 16:23:50 s
,而执行完第五个任务已经是2024-11-24 16:23:53 s
了,超过任务的执行时间,所以会立马执行,执行时间3s
,执行完后往队列里面添加一个2024-11-24 16:23:55 s
的任务,当前时间是2024-11-24 16:23:56 s
- …
其实上面的解释中可以把后面的毫秒去掉,这样比较方便理解
4. 小结
上面就是几个 API 了,其中固定速率的和 Timer
的实现方式有点不同,后续文章中,我会对 ScheduledThreadPoolExecutor
的源码进行详细的解析,当然在解析 ScheduledThreadPoolExecutor
之前首先需要知道下线程池的工作原理和工作流程,所以下一篇文章,我会说下线程池的工作原理