【小家java】java5新特性(简述十大新特性) 重要一跃
【小家java】java6新特性(简述十大新特性) 鸡肋升级
【小家java】java7新特性(简述八大新特性) 不温不火
【小家java】java8新特性(简述十大新特性) 饱受赞誉
【小家java】java9新特性(简述十大新特性) 褒贬不一
【小家java】java10新特性(简述十大新特性) 小步迭代
【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本
【小家java】Java中的线程池,你真的用对了吗?(教你用正确的姿势使用线程池)
小家Java】一次Java线程池误用(newFixedThreadPool)引发的线上血案和总结
【小家java】BlockingQueue阻塞队列详解以及5大实现(ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue…)
【小家java】用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)
定时任务就是在指定时间执行程序,或周期性执行计划任务。Java中实现定时任务的方法有很多,本文从从JDK自带的一些方法来实现定时任务的需求。
本文先介绍Java最原始的解决方案:Timer和TimerTask
Timer和TimerTask可以作为线程实现的第三种方式,在JDK1.3的时候推出。但是自从JDK1.5之后不再推荐时间,而是使用ScheduledThreadPoolExecutor代替
public class Timer {}
// TimerTask 是个抽象类
public abstract class TimerTask implements Runnable {}
Timer运行在后台,可以执行任务一次,或定期执行任务。TimerTask类继承了Runnable接口,因此具备多线程的能力。一个Timer可以调度任意多个TimerTask,所有任务都存储在一个队列中顺序执行,如果需要多个TimerTask并发执行,则需要创建两个多个Timer。
很显然,一个Timer定时器,是单线程的
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
//1、设定两秒后执行任务
//timer.scheduleAtFixedRate(new MyTimerTask1(), 2000,1000);
//2、设定任务在执行时间执行,本例设定时间13:57:00
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date time = dateFormatter.parse("2018/11/04 18:40:00");
//让在指定的时刻执行(如果是过去时间会立马执行 如果是将来时间 那就等吧)
timer.schedule(new MyTimerTask1(), time);
}
//被执行的任务必须继承TimerTask,并且实现run方法
static class MyTimerTask1 extends TimerTask {
public void run() {
System.out.println("爆炸!!!");
}
}
相关API简单介绍(毕竟已经不重要了):
schedule(TimerTask task, long delay, long period) --指定任务执行延迟时间
schedule(TimerTask task, Date time, long period) --指定任务执行时刻
scheduleAtFixedRate(TimerTask task, long delay, long period)
scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
这里需要注意区别:
调用Timer.cancle()方法。可以在程序任何地方调用,甚至在TimerTask中的run方法中调用;
设置Timer对象为null,其会自动终止;
用System.exit方法,整个程序终止。
下面例子:
备注:该示例在某些特殊的场景会很有用的,比如守护监控、守护检查等等
/**
* 定时器
*
* @author fangshixiang
* @description //
* @date 2019/1/22 17:55
*/
public class TaskTest {
/**
* 需求描述:满足条件后启动一个定时任务,再满足另外一个条件后停止此定时任务
* (阶段性定时任务)
* 备注:若单线程就能搞定,就使用timer即可,若需要多线程环境,请使用JDK5提供的ScheduledThreadPoolExecutor
*/
public static void main(String[] args) {
Timer timer = new Timer();
// 三秒后开始执行任务,每隔2秒执行一次 当执行的总次数达到10此时,停止执行
timer.schedule(new Task(timer, 10), 3 * 1000, 2000);
}
}
class Task extends TimerTask {
private Timer timer;
private int exeCount; //此处没有线程安全问题
public Task(Timer timer, int exeCount) {
this.timer = timer;
this.exeCount = exeCount;
}
private int i = 1;
@Override
public void run() {
System.out.println("第" + i++ + "次执行任务");
//处理业务逻辑start...
//处理业务逻辑end...
//若满足此条件 退出此线程
if (i > exeCount) {
this.timer.cancel();
System.out.println("#### timer任务程序结束 ####");
}
}
}
输出:
第1次执行任务
第2次执行任务
第3次执行任务
第4次执行任务
第5次执行任务
第6次执行任务
第7次执行任务
第8次执行任务
第9次执行任务
第10次执行任务
#### timer任务程序结束 ####
ScheduledThreadPoolExecutor解决了上述所有问题~
ScheduledThreadPoolExecutor是JDK1.5以后推出的类,用于实现定时、重复执行的功能,官方文档解释要优于Timer。
构造方法:
ScheduledThreadPoolExecutor(int corePoolSize) //使用给定核心池大小创建一个新定定时线程池
ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactorythreadFactory) //使用给定的初始参数创建一个新对象,可提供线程创建工厂
需要手动传入线程工厂的,可以这么弄:
private final static ScheduledThreadPoolExecutor schedual = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
private AtomicInteger atoInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("xxx-Thread " + atoInteger.getAndIncrement());
return t;
}
});
ScheduledThreadPoolExecutor还提供了非常灵活的API,用于执行任务。其任务的执行策略主要分为两大类:
①在一定延迟之后只执行一次某个任务;
②在一定延迟之后周期性的执行某个任务;
如下是其主要API:
// 执行一次
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
// 周期性执行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay, long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay, long period, TimeUnit unit);
第一个和第二个方法属于第一类,即在delay指定的延迟之后执行第一个参数所指定的任务,区别在于,第二个方法执行之后会有返回值,而第一个方法执行之后是没有返回值的。
第三个和第四个方法则属于第二类,即在第二个参数(initialDelay)指定的时间之后开始周期性的执行任务,执行周期间隔为第三个参数指定的时间。
但是这两个方法的区别在于:第三个方法执行任务的间隔是固定的,无论上一个任务是否执行完成(也就是前面的任务执行慢不会影响我后面的执行
)。而第四个方法的执行时间间隔是不固定的,其会在周期任务的上一个任务执行完成之后才开始计时,并在指定时间间隔之后才开始执行任务。
public class ScheduledThreadPoolExecutorTest {
private ScheduledThreadPoolExecutor executor;
private Runnable task;
@Before
public void before() {
executor = initExecutor();
task = initTask();
}
private ScheduledThreadPoolExecutor initExecutor() {
return new ScheduledThreadPoolExecutor(2);;
}
private Runnable initTask() {
long start = System.currentTimeMillis();
return () -> {
print("start task: " + getPeriod(start, System.currentTimeMillis()));
sleep(SECONDS, 10);
print("end task: " + getPeriod(start, System.currentTimeMillis()));
};
}
@Test
public void testFixedTask() {
print("start main thread");
executor.scheduleAtFixedRate(task, 15, 30, SECONDS);
sleep(SECONDS, 120);
print("end main thread");
}
@Test
public void testDelayedTask() {
print("start main thread");
executor.scheduleWithFixedDelay(task, 15, 30, SECONDS);
sleep(SECONDS, 120);
print("end main thread");
}
private void sleep(TimeUnit unit, long time) {
try {
unit.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private int getPeriod(long start, long end) {
return (int)(end - start) / 1000;
}
private void print(String msg) {
System.out.println(msg);
}
}
第一个输出:
start main thread
start task: 15
end task: 25
start task: 45
end task: 55
start task: 75
end task: 85
start task: 105
end task: 115
end main thread
第二个输出:
start main thread
start task: 15
end task: 25
start task: 55
end task: 65
start task: 95
end task: 105
end main thread
从结果,现在重点说说这两者的区别:
是以上一个任务结束时开始计时,period时间过去后,立即执行, 由上面的运行结果可以看出,第一个任务开始和第二个任务开始的间隔时间是 第一个任务的运行时间+period(永远是这么多)
注意: 通过ScheduledExecutorService执行的周期任务,如果任务执行过程中抛出了异常,那么过ScheduledExecutorService就会停止执行任务,且也不会再周期地执行该任务了。所以你如果想保住任务都一直被周期执行,那么catch一切可能的异常。
钩子方法decorateTask(Runnable, RunnableScheduledFuture)
用于对执行的任务进行装饰,该方法第一个参数是调用方传入的任务实例,第二个参数则是使用ScheduledFutureTask对用户传入任务实例进行封装之后的实例。这里需要注意的是,在ScheduledFutureTask对象中有一个heapIndex变量,该变量用于记录当前实例处于队列数组中的下标位置,该变量可以将诸如contains(),remove()等方法的时间复杂度从O(N)降低到O(logN),因而效率提升是比较高的,但是如果这里用户重写decorateTask()方法封装了队列中的任务实例,那么heapIndex的优化就不存在了,因而这里强烈建议是尽量不要重写该方法,或者重写时也还是复用ScheduledFutureTask类。