Java 中的 ScheduledExecutorService 接口用来实现延迟执行或者定时执行的任务。在阅读 RocketMQ 源码(release-4.7.1版本)的过程中,发现很多地方都是使用的 ScheduledExecutorService 来实现定时任务。比如,在 broker 启动过程,BrokerController 类中使用 scheduledExecutorService 执行 broker 启动后的定时任务,比如消息消费偏移量 offset 的持久化定时任务,大致如下:
package org.apache.rocketmq.broker;
public class BrokerController {
/**
* 这里省略一些字段
*/
/**
* 使用 Executors.newSingleThreadScheduledExecutor 创建单线程的定时调度任务线程池
*/
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
"BrokerControllerScheduledThread"));
/**
* broker 启动的初始化方法
*/
public boolean initialize() throws CloneNotSupportedException {
// 省略部分代码
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.getBrokerStats().record();
} catch (Throwable e) {
log.error("schedule record error.", e);
}
}
}, initialDelay, period, TimeUnit.MILLISECONDS);
// broker 启动10s后,默认每隔5s钟持久化一次消费消息偏移量offset信息
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
// 持久化消费偏移量,将 ConsumerOffsetManager 序列化为json文件保存在本地
BrokerController.this.consumerOffsetManager.persist();
} catch (Throwable e) {
log.error("schedule persist consumerOffset error.", e);
}
}
}, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.consumerFilterManager.persist();
} catch (Throwable e) {
log.error("schedule persist consumer filter error.", e);
}
}
}, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS);
// 后面还有很多.......
}
}
所以觉得有必要学习理解 ScheduledExecutorService。大致看了一下 ScheduledExecutorService 实现类 ScheduledThreadPoolExecutor 的代码,跟我们常用的 ThreadPoolExecutor 线程池类还是有一些差异的,里面有一个基于堆实现的优先队列,后面要再深入学习下,本篇先学习 ScheduledExecutorService 的使用,先学会使用,再分析原理。
本文使用JDK版本:JDK8.
一、ScheduledExecutorService 接口定义
首先,ScheduledExecutorService 是一个接口,它实现了 ExecutorService 接口。
在 ScheduledExecutorService 接口中又新定义了下面四个方法:
1、public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
在指定延迟时间后执行一个 Runnable 任务。因为是 Runnable ,返回值 ScheduledFuture.get() 返回值为 null。
2、 <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
在指定延迟时间后执行一个 Callable 任务。因为是 Callable ,所以 ScheduledFuture.get() 有返回值。
3、ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
创建并执行一个在给定初始延迟 (initialDelay) 后首次启动执行的定时任务,任务执行具有给定的周期 (period);
也就是将在 initialDelay 后开始执行,然后在 initialDelay + period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。
如果任务的任何一个执行遇到异常,则后续执行都会被取消。相反,任务正常执行的话,只能通过线程池的取消或终止操作来终止该任务。
如果此任务的任何一个执行要花费比其周期更长的时间,则后续的执行将会被推迟,不会出现两个任务同时执行。
4、ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
创建并执行一个在给定初始延迟 (initialDelay) 后首次启动执行的任务,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟(delay)。
如果任务的任一执行遇到异常,就会取消后续任务执行。相反,任务正常执行的话,只能通过线程池的取消或终止操作来终止该任务。
二、ScheduledExecutorService 接口使用
Executors 类中封装了几个方法返回 ScheduledExecutorService 的实现:
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public static ScheduledExecutorService unconfigurableScheduledExecutorService(ScheduledExecutorService executor) {
if (executor == null)
throw new NullPointerException();
return new DelegatedScheduledExecutorService(executor);
}
可以看到底层实现主要借助两个类: DelegatedScheduledExecutorService 和 ScheduledThreadPoolExecutor。其中 newSingle- 开头的方法返回的线程池实现只有一个核心线程,也就用这一个线程执行提交的延迟或者定时任务。我们也使用 rocketmq 中用的 Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory):
Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
public class ScheduledExecutorServiceTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
"TestScheduledThread"));
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("我是间隔1s执行的任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
}
}, 0L, 1, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("我是间隔5s执行的任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
}
}, 3L, 5, TimeUnit.SECONDS);//延迟 3s 后再执行定时任务
}
}
class ThreadFactoryImpl implements ThreadFactory {
private final AtomicLong threadIndex = new AtomicLong(0);
private final String threadNamePrefix;
private final boolean daemon;
public ThreadFactoryImpl(final String threadNamePrefix) {
this(threadNamePrefix, false);
}
public ThreadFactoryImpl(final String threadNamePrefix, boolean daemon) {
this.threadNamePrefix = threadNamePrefix;
this.daemon = daemon;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, threadNamePrefix + this.threadIndex.incrementAndGet());
thread.setDaemon(daemon);
return thread;
}
}
scheduledExecutorService.scheduleAtFixedRate 方法
注意:我们上面采用的是 scheduledExecutorService.scheduleAtFixedRate 方法。
运行 main 方法一段时间后,结束运行,观察控制台日志:
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374865
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374866
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374867
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374868
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374868
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374869
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374870
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374871
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374872
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374873
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374873
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374874
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374875
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374876
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374877
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374878
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374878
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617374879
可以看到 1s 中的任务每一秒执行一次,5s 任务首次执行在延迟 3s 钟之后,每 5s 执行一次。
scheduledExecutorService.scheduleAtFixedRate 方法如果某个定时任务某次执行异常了,那么该任务后续将不会再执行,我们测试一下:
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("我是间隔5s执行的任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
//这里抛出异常
throw new RuntimeException();
}
}, 3L, 5, TimeUnit.SECONDS);
控制台输出:
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375180
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375181
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375182
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375183
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375183
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375184
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375185
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375186
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375187
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375188
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375189
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375190
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375191
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375192
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375193
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375194
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375195
可以看出,5s 的任务只执行了一次,因为抛出了异常便不再执行了。 1s 的任务则不受影响。所以我们最好要像 rocketmq 中那样使用,用 try - catch 包裹住任务执行代码,并且打出 error 级别的日志:
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try{
Thread thread = Thread.currentThread();
System.out.println("我是间隔5s执行的任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
throw new RuntimeException();
} catch (Exception e){
System.out.println(e);//模拟输出 error 信息
}
}
}, 3L, 5, TimeUnit.SECONDS);
修改之后,控制台输出:
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375547
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375548
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375549
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375550
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375550
java.lang.RuntimeException
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375551
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375552
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375553
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375554
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375555
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375555
java.lang.RuntimeException
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375556
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375557
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375558
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375559
5s 的任务虽然第一次抛出了异常,但是 catch 处理异常后,后续任务仍会正常执行。
再测试一下,如果任务执行时间超出了定时周期的情况:
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
"TestScheduledThread"));
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("我是间隔1s执行的任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
try {
// 模拟任务执行耗时5s
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 0L, 1, TimeUnit.SECONDS);
}
控制台输出结果:
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375850
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375855
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375860
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375865
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375870
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617375875
可以看出:scheduledExecutorService.scheduleAtFixedRate 方法如果定时任务执行时间超过了定时周期时长,则后续的任务执行将会被推迟,不会出现两个任务同时执行。如果在这种情况下,我们再把上面首次延迟 3s 执行的 5s 的定时任务加上,会有什么现象:
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376360
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376365
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376370
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376375
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376380
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376380
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376385
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376390
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376395
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376400
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376405
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376405
我是间隔1s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617376410
可以看出,因为 1s 定时任务执行时间超过了执行周期,影响到了 5s 定时任务的周期执行,控制台显示 5s 的任务隔了 25 秒才执行,并且首次延迟的时间也非常不准确了,这是因为我们使用的是Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory) ,它底层只有一个线程处理这些定时任务,所以导致这种问题的发生。如果我们改为如下设置多个核心线程的ScheduledExecutorService 实现就可以避免这个问题:
//使用两个核心线程
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
控制台输出结果,1s 和 5s 两个定时任务执行都恢复正常了:
我是间隔1s执行的任务,线程name:pool-1-thread-1,执行时间戳:1617377444
我是间隔5s执行的任务,线程name:pool-1-thread-2,执行时间戳:1617377447
我是间隔1s执行的任务,线程name:pool-1-thread-1,执行时间戳:1617377449
我是间隔5s执行的任务,线程name:pool-1-thread-2,执行时间戳:1617377452
我是间隔1s执行的任务,线程name:pool-1-thread-1,执行时间戳:1617377454
我是间隔5s执行的任务,线程name:pool-1-thread-2,执行时间戳:1617377457
我是间隔1s执行的任务,线程name:pool-1-thread-1,执行时间戳:1617377459
我是间隔5s执行的任务,线程name:pool-1-thread-2,执行时间戳:1617377462
我是间隔1s执行的任务,线程name:pool-1-thread-1,执行时间戳:1617377464
scheduledExecutorService.scheduleWithFixedDelay 方法
这个方法每次执行任务是有指定的时间间隔:
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
"TestScheduledThread"));
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("我是每次执行完毕间隔2s执行的任务,线程name:"+ thread.getName() + ",开始执行时间戳:" + System.currentTimeMillis() / 1000);
// 随机数模拟每次执行耗时不同
int random = new Random().nextInt(6);
try {
Thread.sleep(random * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是每次执行完毕间隔2s执行的任务,线程name:"+ thread.getName() + ",执行完毕时间戳:" + System.currentTimeMillis() / 1000);
}
}, 0L, 2, TimeUnit.SECONDS);
}
控制台输出,每次执行完毕后隔 2s 开启下一次执行:
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,开始执行时间戳:1617378188
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,执行完毕时间戳:1617378193
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,开始执行时间戳:1617378195
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,执行完毕时间戳:1617378199
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,开始执行时间戳:1617378201
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,执行完毕时间戳:1617378206
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,开始执行时间戳:1617378208
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,执行完毕时间戳:1617378210
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,开始执行时间戳:1617378212
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,执行完毕时间戳:1617378214
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,开始执行时间戳:1617378216
我是每次执行完毕间隔2s执行的任务,线程name:TestScheduledThread1,执行完毕时间戳:1617378217
ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
这个方法需要注意的是,ScheduledFuture 继承了 Future 接口,scheduledFuture.get() 也会阻塞调用方线程直至取到结果。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
"TestScheduledThread"));
ScheduledFuture scheduledFuture = scheduledExecutorService.schedule(new Callable() {
@Override
public Integer call() {
Thread thread = Thread.currentThread();
System.out.println("我是延迟10s执行的一次性任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 250;
}
}, 10L, TimeUnit.SECONDS);
// scheduledFuture.get() 会阻塞调用方线程,这里是 main 线程
System.out.println("scheduledFuture.get():" + scheduledFuture.get());
System.out.println("hello world");
}
方法混合在一起使用
把上面几种方法混合在一起调用:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(
"TestScheduledThread"));
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("我是间隔5s执行的任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
}
}, 0L, 5, TimeUnit.SECONDS);
ScheduledFuture scheduledFuture = scheduledExecutorService.schedule(new Callable() {
@Override
public Integer call() {
Thread thread = Thread.currentThread();
System.out.println("我是延迟10s执行的一次性任务,线程name:"+ thread.getName() + ",执行时间戳:" + System.currentTimeMillis() / 1000);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 250;
}
}, 10L, TimeUnit.SECONDS);
// scheduledFuture.get() 会阻塞调用方线程,这里是 main 线程
System.out.println("scheduledFuture.get():" + scheduledFuture.get());
System.out.println("hello world");
}
输出结果:
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379143
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379148
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379153
我是延迟10s执行的一次性任务,线程name:TestScheduledThread1,执行时间戳:1617379153
scheduledFuture.get():250
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379158
hello world
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379163
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379168
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379173
我是间隔5s执行的任务,线程name:TestScheduledThread1,执行时间戳:1617379178
运行非常好~
感觉 ScheduledExecutorService 很强大,后续要分析一下上述两个类 DelegatedScheduledExecutorService 和 ScheduledThreadPoolExecutor 的底层实现。