1Java实现一个定时器
1.1 定时器的作用和需求
在某些场景下,常常需要定时的功能,如商城的到整点开启商品秒杀功能,这个功能就可以使用定时器来调用一个方法完成,将当前时间到指定时间的差值作为定时器的延时时间,等定时器等待了确认的延时时间后,将调用一个方法,把秒杀系统的页面呈现给用户
因此,这个定时器应该实现:
1,制定一个时间间隔
2,在到达规定时间后,能自动调用某个任务
重点在于:定时的精确性,应该尽可能地提高定时精度
不要受到执行任务的干扰
1.2 定时器的简单实现
最简单的方法是当开启定时器后,直接执行指定的任务即可
/**
* @author 雫
* @date 2021/4/17 - 12:21
* @function
*/
public abstract class SimpleTimer implements Runnable {
// 定时调用时间 每隔time间隔 执行指定任务
private long time;
// 决定定时器是否工作
private volatile boolean goon;
// 抽象方法 待完成
public abstract void job() throws InterruptedException;
public SimpleTimer() {}
public void setTime(long time) {
this.time = time;
}
// 开启定时器
public void start() {
if(this.goon == true) {
return;
}
this.goon = true;
new Thread(this).start();
}
// 关闭定时器
public void terminate() {
if(this.goon == false) {
return;
}
this.goon = false;
}
@Override
public void run() {
while (this.goon) {
try {
Thread.sleep(this.time);
job();
} catch (InterruptedException e) {}
}
}
}
测试:
这里要求定时器只要开始工作后,每隔1s输出一次当前时间,5s后关闭定时器,理想的情况下,是每次输出的时间比上次多1000ms,查看上述的输出结果:
1618635216238
1618635217241 ->1003ms
1618635218254 ->1013ms
1618635219264 ->1010ms
1618635220276 ->1012ms
根据上述的输出结果,可以看到误差在10ms左右,现在我们看另一种情况,更改job()内的代码,让job()每次输出完时间后,随机等待一段时间:
查看输出结果:
1618635520403
1618635522244->1841
1618635523365->1121
1618635524394->1029
本次的输入,job()不但少执行了一次,而且最大的误差达到了841ms,这样的定时器是不精确,不能用的
问题在于:
/*
* 下面的定时器是存在巨大缺陷的
* 理想是每隔time时间段后执行一次job
* 哪怕上次的job没有执行完 时间到了也要开启下一次job
* 但是这里的却是每隔time+job所需时间后 才能开启下一次任务
* 执行job占用了时间 因此定时器不再精确
* */
@Override
public void run() {
while (this.goon) {
try {
Thread.sleep(this.time);
job();
} catch (InterruptedException e) {}
}
}
因此一个线程是不够用的,定时功能和调用方法的功能,应该独立运行,定时器的作用是时间到了后,让另一个线程去调用方法
1.3 单线程执行任务的定时器
开启一个线程作为定时器,只使用一个线程来执行任务,每次执行完任务后任务线程进入休眠,需要执行下一次任务时,再唤醒任务线程,但是执行任务所需的时间必须小于定时间隔
任务接口:
/**
* @author 雫
* @date 2021/4/17 - 13:15
* @function 任务接口
*/
public interface Task {
void job();
}
Task的实现类对象应该作为工作线程的成员
以便工作线程被唤醒后,开始调用job()来完成工作,工作线程类:
/**
* @author 雫
* @date 2021/4/17 - 13:27
* @function 单线程执行任务
*/
public class SingleThreadTask implements Runnable {
private Task task;
private volatile boolean goon;
public SingleThreadTask(Task task) {
this.task = task;
this.goon = false;
}
void start() {
this.goon = true;
// 开启工作线程
new Thread(this).start();
}
void stop() {
this.goon = false;
}
// 执行任务的线程
@Override
public void run() {
/*
* 起始时阻塞自己,被唤醒后再执行任务,执行完后再阻塞自己
* 该工作线程被唤醒时,才执行job() 执行完后阻塞自己 再等待被唤醒
* 被唤醒的时机就是定时器达到了下一个时间间隔
* */
while (this.goon) {
synchronized (this) {
try {
this.wait();
this.task.job();
} catch (InterruptedException e) {}
}
}
}
}
工作线程类应该作为定时器类的成员,时间间隔到达时,唤醒工作线程
/**
* @author 雫
* @date 2021/4/17 - 13:16
* @function 单线程执行任务的定时器
*/
public class SingleThreadTimer implements Runnable {
private long time;
private volatile boolean goon;
private Task task;
// 工作线程
private SingleThreadTask singleThreadTask;
// 对象锁
private volatile Object lock;
public SingleThreadTimer() {
this.lock = new Object();
}
public SingleThreadTimer(long time, Task task) {
this();
this.time = time;
this.task = task;
}
public void setTime(long time) {
this.time = time;
}
public void setTask(Task task) {
this.task = task;
}
public void start() {
if(this.task == null) {
System.out.println("未设置任务");
return;
}
if(this.goon) {
return;
}
// 开启工作线程,工作线程此时处于阻塞状态,等待被唤醒后再工作
singleThreadTask = new SingleThreadTask(this.task);
singleThreadTask.start();
this.goon = true;
// 开启定时器
new Thread(this).start();
}
public void terminate() {
if(!this.goon) {
return;
}
this.goon = false;
}
// 开启定时线程
@Override
public void run() {
while (this.goon) {
synchronized (this.lock) {
try {
/*
* 定时器每隔time时间片段后 去唤醒工作线程
* 工作线程不再阻塞,开始执行job 执行完后将自己阻塞
* 等待下一次被唤醒
* */
this.lock.wait(this.time);
} catch (InterruptedException e) {}
synchronized (this.singleThreadTask) {
this.singleThreadTask.notify();
}
}
}
// 结束后 关闭任务线程
this.singleThreadTask.stop();
}
}
测试:
执行结果:
1618639296949
1618639297954->1005
1618639298963->1009
1618639299974->1011
1618639300986->1012
执行结果的无参大概在10ms左右,使用单线程执行任务可以避免创建过多的线程,避免系统资源被浪费,始终只有一个线程在工作
但是单线程执行任务的时间必须要小于定时间隔,如果时间间隔是1000ms,执行一个任务需要1500ms,当第一个任务线程执行到1000ms时,定时线程尝试去唤醒任务线程,此时任务线程还在执行,唤醒失败,且本次的任务执行完后,才能有机会执行下一次任务
单线程执行任务是不稳定的
1.4 多线程执行任务的定时器
开启一个线程作为定时器,每隔一段时间开启一个任务线程,不管上次的任务有没有结束,本次将会开启一个新线程来执行任务
/**
* @author 雫
* @date 2021/4/17 - 13:15
* @function 任务接口
*/
public interface Task {
void job();
}
/**
* @author 雫
* @date 2021/4/17 - 14:09
* @function 多线程执行任务
*/
public class ManyThreadTimer implements Runnable {
private long time;
private volatile boolean goon;
private Task task;
public ManyThreadTimer(long time, Task task) {
this.time = time;
this.task = task;
}
public void setTime(long time) {
this.time = time;
}
public void setTask(Task task) {
this.task = task;
}
public void start() {
if(goon) {
return;
}
this.goon = true;
new Thread(this).start();
}
public void terminate() {
if(!goon) {
return;
}
this.goon = false;
}
/*
* 定时线程每经过一段时间,开启一个执行任务的线程
* 执行任务的时间与定时时间无关
* */
@Override
public void run() {
while (this.goon) {
try {
Thread.sleep(this.time);
new Thread(new Runnable() {
@Override
public void run() {
task.job();
}
}).start();
} catch (InterruptedException e) {}
}
}
}
测试:
输出结果:
1618640693032
1618640694036->1004
1618640695050->1004
1618640696062->1012
1618640697070->1008
误差保持在5-10ms,并且使用多线程执行任务,定时器与任务线程是完全分开的,无论任务线程要执行多久,定时器只要时间间隔到了,就会开启新的线程,不管上一个线程有没有执行完毕
因为创建了多个线程,采用线程池来管理:
/**
* @author 雫
* @date 2021/4/17 - 14:09
* @function 多线程执行任务
*/
@SuppressWarnings("all")
public class ManyThreadTimer implements Runnable {
private long time;
private volatile boolean goon;
private Task task;
// 线程池保存多个线程
private ThreadPoolExecutor threadPoolExecutor;
public ManyThreadTimer(long time, Task task) {
this.time = time;
this.task = task;
threadPoolExecutor = new ThreadPoolExecutor(5,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10));
}
public void setTime(long time) {
this.time = time;
}
public void setTask(Task task) {
this.task = task;
}
public void start() {
if(goon) {
return;
}
this.goon = true;
new Thread(this).start();
}
public void terminate() {
if(!goon) {
return;
}
this.goon = false;
}
/*
* 定时线程每经过一段时间,开启一个执行任务的线程
* 执行任务的时间与定时时间无关
* */
@Override
public void run() {
while (this.goon) {
try {
Thread.sleep(this.time);
// 使用线程池 避免多次重复创建线程
this.threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
task.job();
}
});
} catch (InterruptedException e) {}
}
// 关闭定时器的同时关闭线程池
this.threadPoolExecutor.shutdown();
}
}
1.5 定时器小结
为了精准地在一段时间后执行某个任务,采用一个线程控制时间,每隔一段时间开启一个新线程就可以顺利执行,这样的方式不论任务需要执行多久,时间到了,定时器也会开启新的线程执行任务,配合上线程池,可以避免重复创建线程,需要注意的是执行的任务运行在多线程环境中
这样的定时器可以用于B/S模式中的轮询和各种与指定时间有关的功能