目录
背景
最近在看若依项目,看到代码中有使用ScheduledExecutorService的地方,但是之前没有用过,因此去网上搜索到了这篇文档,所以将其记录下来,方便以后使用
对于ScheduledExecutorService来说,我平时没有用过,它的最大优点除了线程池基本特性以外,可以实现循环或延迟任务,听起来好像挺像定时器的,但是相比于定时器,它还有一个优点,可以通过代码控制任务依次执行,而不是上次还没执行完毕,下次就已经开始执行了。
ScheduledExecutorService是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说任务是并发执行、互不影响的。
需要注意,只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是出于轮询任务的状态。
创建ScheduledExecutorService
1、创建方式1
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
2、创建方式2
// 我们可以对线程进行增强
private static ScheduledExecutorService scheduler;
public static void main(String[] args) throws Exception {
scheduler = Executors.newScheduledThreadPool(5, new ThreadFactory() {
private AtomicInteger counter = new AtomicInteger(0);
//可以在这里对线程做一些操作
@Override
public Thread newThread(Runnable r) {
int count = counter.incrementAndGet();
System.out.println("线程创建counter = " + count);
Thread thread = new Thread(r);
thread.setName("测试线程"+count);
return thread;
}
});
System.out.println("main thread time : " + formatDateToString(new Date()));
// 循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
scheduler.scheduleAtFixedRate(((
new Runnable() {
@Override
public void run() {
System.out.println(
" 开始 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
}
})),
1,5,
TimeUnit.SECONDS);
}
public static String formatDateToString(Date time) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(time);
}
说明:
3、创建方式3
scheduler = Executors.newScheduledThreadPool(5,new ThreadFactoryBuilder()
.setNameFormat("测试线程-%d")
// .setDaemon(true) //这个参数是设置为守护线程 也叫 服务线程
.build());
定义:守护线程–也称“服务线程”,在没有用户线程可服务时会自动离开。优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
设置:通过setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon方法。用来设置线程
生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。那Java的守护线程是什么样子的呢。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。
比如: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
当java虚拟机中没有非守护线程在运行的时候,java虚拟机会关闭。当所有常规线程运行完毕以后,守护线程不管运行到哪里,虚拟机都会退出运行。所以你的守护线程最好不要写一些会影响程序的业务逻辑。否则无法预料程序到底 会出现什么问题。
常用方法
1、scheduleAtFixedRate()
1.1、概念
- scheduleAtFixedRate是以上一个任务开始的时间计时,120秒过去后,检测上一个任务是否执行完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行,之前我看到一些博客直接说的是按固定频率执行,但是不严谨,要注意如果你的执行任务的时间超过了间隔时间,那么就会变成一直连续循环执行,间隔时间的参数其实已经没发挥出作用了,其实一般我们也不会犯这样的错误,只是要清楚,固定频率是有条件的就行。
- 关于定时线程池,好多人认为设置好频率(比如1Min),它会按照这个间隔按部就班的工作。但是,如果其中一次调度任务卡住的话,不仅这次调度失败,而且整个线程池也会停在这次调度上。 因此,注意在使用的时候涉及一些可能因为网络或者其他一些原因导致异常,要注意用try…catch…捕获
1.2、使用举例
// scheduleAtFixedRate 正常使用 延迟时间1S,执行任务时间 1S,间隔时间3S
private static ScheduledExecutorService scheduler;
public static void main(String[] args) throws Exception {
scheduler = Executors.newScheduledThreadPool(5);
// 循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
scheduler.scheduleAtFixedRate(((
new Runnable() {
@Override
public void run() {
System.out.println(
" 开始 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
try {
Thread.sleep(1000);
System.out.println(
" 结束 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
})),
0, 3,
TimeUnit.SECONDS);
}
public static String formatDateToString(Date time) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(time);
}
说明:
可以看到任务开始时间的间隔 为我们设置的 period 间隔时间3s,计算方式就是 initialDelay 初始延迟时间(第一次任务执行的间隔时间) + n * period(间隔时间) 0s 3s 6s 来计算
1.3、异常情况举例
1.3.1、设置的 period间隔时间 小于 任务执行时间,导致任务连续执行
private static ScheduledExecutorService scheduler;
public static void main(String[] args) throws Exception {
scheduler = Executors.newScheduledThreadPool(5);
// 循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
scheduler.scheduleAtFixedRate(((
new Runnable() {
@Override
public void run() {
System.out.println(
" 开始 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
try {
Thread.sleep(3000);
System.out.println(
" 结束 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
})),
0, 1,
TimeUnit.SECONDS);
}
public static String formatDateToString(Date time) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(time);
}
说明:
可以看到其实间隔时间已经变成了 任务执行时间来控制,但是一般来说,很少有任务执行时间超过间隔时间,但是这个知识点还是要知道。
1.3.2、没有进行异常捕获,导致后续调度任务禁止
private static ScheduledExecutorService scheduler;
public static void main(String[] args) throws Exception {
scheduler = Executors.newScheduledThreadPool(5);
// 循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
scheduler.scheduleAtFixedRate(((
new Runnable() {
@Override
public void run() {
System.out.println(
" 开始 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
try {
Thread.sleep(3000);
System.out.println(
" 结束 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
} catch (InterruptedException e) {
e.printStackTrace();
}
//模拟抛出异常
if (1 == 1) {
throw new RuntimeException("异常");
}
}
})),
0, 1,
TimeUnit.SECONDS);
Thread.sleep(20000);
System.out.println(
" 主线程 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
}
public static String formatDateToString(Date time) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(time);
}
执行结果:
1.4、实战说明
在公司编码的时候,遇到这么一个问题,我们需要把大量知识文档文本发给第三方进行解析,然后把第三方解析结果拿回来存储到本地,但是不知道第三方啥时候解析完成,因此我们需要自动定时去调用第三方接口获取所有未解析文档的解析结果,当时使用的是TaskService,然后间隔一段时间执行一次全量数据请求,但是这有一个弊端
也就是如果在间隔时间之内没有将所有解析结果从第三方拿回来,那么下一次任务执行开始的时候可能上次任务还没结束,如果这种情况出现次数比较多,那么第三方接口可能同时需要处理我们多个请求,这可能会造成第三方接口的崩溃,所以可以这种方式解决该问题,那样第三方接口同时只用处理我们的一次请求
2、scheduleWithFixedDelay()
2.1、概念
以上一个任务结束时开始计时,然后立即执行
2.2、使用举例
private static ScheduledExecutorService scheduler;
public static void main(String[] args) throws Exception {
scheduler = Executors.newScheduledThreadPool(5);
// 循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
scheduler.scheduleWithFixedDelay(((
new Runnable() {
@Override
public void run() {
System.out.println(
" 开始 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
try {
Thread.sleep(3000);
System.out.println(
" 结束 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
})),
0, 1,
TimeUnit.SECONDS);
}
public static String formatDateToString(Date time) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(time);
}
说明:
scheduleWithFixedDelay以上一次任务的结束时间 + 延迟时间 = 下一次任务的开始时间。
3、schedule()
3.1、概念
只在延迟时间后 运行一次
3.2、使用举例
private static ScheduledExecutorService scheduler;
public static void main(String[] args) throws Exception {
scheduler = Executors.newScheduledThreadPool(5);
System.out.println("main thread time : " + formatDateToString(new Date()));
// 循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
scheduler.schedule(((
new Runnable() {
@Override
public void run() {
System.out.println(
" 开始 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
try {
Thread.sleep(1000);
System.out.println(
" 结束 threadId = "
+ Thread.currentThread().getId()
+ ",,,threadName = " + Thread.currentThread().getName()
+ ",,,时间" + formatDateToString(new Date())
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
})),
5,
TimeUnit.SECONDS);
}
public static String formatDateToString(Date time) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(time);
}
说明: