本篇文章的相关代码本人已经上传至本人的 gitee Timer。
2.标准库中的定时器运用
import java.util.Timer;
import java.util.TimerTask;
public class ThreadDemo {
public static void main(String[] args) {
System.out.println("程序启动");
//Timer 是标准库的定时器
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务1");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务2");
}
},2000);
}
}
根据上面标准库中对定时器的使用,我们可以知道,要实现一个定时器需要实现以下功能:
针对第一点:要在指定时间内执行线程。
要满足这个条件,需要单独在定时器内部设定一个扫描线程,让这个扫描线程周期性对任务进行扫描,判断是否到达时间,是否执行。
针对第二点:定时器可以注册 N 个任务。
很显然,这里需要一个数据结构来保存。
我们知道,这里的每个任务都带着一个重要元素 时间,并且要求 时间越靠前,越先执行。 到此,已经非常明显,优先级队列无疑是很好的一个选择。
在已知使用 优先级队列 之后,线程扫描也变得更加容易实现,这种情况下,只需要扫描队首元素即可,无序遍历整个队列。
如图所示:
要实现一个阻塞式优先级队列,需要指定元素类型,这里的 任务 可以使用 Runnable 来表示,除此之外,我们还需要描述任务什么时候执行。
根据上面的描述,我们已经有了大致的方向,下面,我们先实现一个类,将 任务 和 时间 进行包装,成为阻塞式优先级队列的元素类型,代码如下:
实现类型方法
class MyTask{
//要执行的任务内容
private Runnable runnable;
//任务在什么时间执行(使用毫秒级时间戳)
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
//获取任务当前的时间
public long getTime(){
return time;
}
//执行任务
public void run(){
runnable.run();
}
}
对于定时器类,需要提供一个 schedule 方法来实现任务的注册,代码如下:
实现创建任务方法
//指定两个参数
//第一个是指定任务
//第二个是指定在多少秒后执行
public void schedule(Runnable runnable,long after){
//这里要注意的是,after 是要在当前时间下,在等待多长时间,这里的时间戳获取当前的时间
MyTask myTask = new MyTask(runnable,System.currentTimeMillis() + after);
queue.put(myTask);
}
上面的这个方法还是比较简单的,对于实现定时器真正比较麻烦的方法,是在扫描线程上。代码如下:
实现扫描线程
//扫描线程
private Thread t = null;
//扫描线程实现
public MyTimer(){
t = new Thread(()->{
while(true){
//取出队首元素,查看队首元素是否到达时间
//如果时间没到,将元素放回到任务队列中
//如果时间到,将任务执行
try {
//取出队首的元素
MyTask myTask = queue.take();
//获取当前的时间
long curTime = System.currentTimeMillis();
//当前的时间与之前设定的时间比较
if(curTime < myTask.getTime()){
//还没到点,不必执行
//假设现在10:00 取出的任务要 11:00 执行
queue.put(myTask);
}else{
//表明时间到了,可以执行任务
myTask.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
到此,代码的大致逻辑已经实现完毕,让我们创建几个线程简单运行一下,如图:
不出所料,出现问题了,正如上面红线表示出来的一样,在这里,我们虽然使用了优先级队列,但是,我们没有设定这个优先级队列如何进行比较。
所以,在这里我们需要 MyTask 类实现 Comparable 接口或者使用 Comparator 单独实现一个比较器。
这里我使用 comparable 接口实现比较,代码如下:
class MyTask implements Comparable<MyTask>{
//要执行的任务内容
private Runnable runnable;
//任务在什么时间执行(使用毫秒级时间戳)
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
//获取任务当前的时间
public long getTime(){
return time;
}
//执行任务
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTask o) {
//返回 小于 0,大于 0,等于 0
//this 比 o 小 返回 <0
//this 比 o 大 返回 >0
//this 等于 o 返回 0
//当前需要的是队首元素是时间最小的元素
return (int) (this.time - o.time);
}
}
代码修改到这里,上面这个比较明显的问题已经解决,但是,代码中仍然潜藏着比较麻烦的问题。
我们知道,计时器会将队首的元素进行获取并且判断任务是否已将到达设定时间,但是,在我们这里的扫描线程这段代码中,会出现当任务的时间没到,会一直重复做拿出塞回的操作。
上述现象称之为 “忙等”。也就是: 等,但是没有闲着。
按理来说,等待是要释放 CPU 资源的。但是忙等,既进行了等待,又占用着 CPU 的资源。
针对当前的情况,忙等显然是不必要的,需要将其修改为阻塞式等待。
呢么此处进行等待使用 sleep 方法可行吗? 时间要设定为多久呢?
假设当前时间为 10:00 队首元素要在 11:00 执行,呢么就直接设定等待 1 小时。这样其实过于大意了,因为在这 1 小时之中,随时都有可能会有新的任务加入,万一加入的任务等待的时间更短呢?
所以 sleep 方法在这里就显得很是死板了。
因此,wait 方法在这里就比较合适了。
首先,使用 wait 等待时,每次有新任务创建时,可以使用 notify 释放一下,重新检查时间,重新计算需要等待的时间。
其次,wait 方法提供一个带有 超时时间 的版本。如果没有新任务,这个版本的 wait 就可以等待到规定时间后进行自动唤醒。
所以,综上所述,这里使用 wait,notify 方法解决问题。
扫描线程中的改动
schedule 方法中的改动
写到这里,定时器中 90% 的问题已经解决,最后我们在考虑一个极端情况,这个情况和随机调度密切相关。
上面的问题中,我们解决了 “忙等” 这个问题,但是,因为线程的随机调度,代码中仍然存在着问题,如下图所示:
正如上面所讲,此时如果在线程被调走这段时间中添加了新的元素,就会出现线程安全问题。
正如上图所示,此时由于扫描线程中的 wait 操作还没有执行。当恰好在这个时间间隙中调用 schedule 方法,此时 schedule 方法中的 notify 方法将其不到任何唤醒 wait 的作用。 但是,任务仍然插入到了队列之中!!
注:
如上图所示,创建任务时,任务的时间设定在此处。
在 schedule 方法中通过构造方法获取该任务的时间。
场景设想:
如上面的解释和场景的设想,不难发现,此时此刻,新任务虽然已经插入队列,并且存在于队首,但是,此时代码中的时间计算仍然以未插入元素前的任务时间为基准进行等待。 新的任务,被错过了。
对上述场景的分析,我们知道,出现线程安全问题中有一点就是,原子性。
在这里 take 操作和 wait 操作并不是原子性的,此时只要在 wait 和 take 之间进行加锁,这个问题就引刃而解了。如图所示:
总得来说,就是确保在每次 notify 的时候保证 wait 的确存在。
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{
//要执行的任务内容
private Runnable runnable;
//任务在什么时间执行(使用毫秒级时间戳)
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
//获取任务当前的时间
public long getTime(){
return time;
}
//执行任务
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTask o) {
//这里主要通过实验观察,无需记背
//返回 小于 0,大于 0,等于 0
//this 比 o 小 返回 <0
//this 比 o 大 返回 >0
//this 等于 o 返回 0
//当前需要的是队首元素是时间最小的元素
return (int) (this.time - o.time);
}
}
//自主实现定时器
class MyTimer{
//扫描线程
private Thread t = null;
//使用一个阻塞式的优先级队列,保存任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
//扫描线程实现
public MyTimer(){
t = new Thread(()->{
while(true){
//取出队首元素,查看队首元素是否到达时间
//如果时间没到,将元素放回到任务队列中
//如果时间到,将任务执行
synchronized (this){
try {
//取出队首的元素
MyTask myTask = queue.take();
//获取当前的时间
long curTime = System.currentTimeMillis();
//当前的时间与之前设定的时间比较
if(curTime < myTask.getTime()){
//还没到点,不必执行
//假设现在10:00 取出的任务要 11:00 执行
queue.put(myTask);
//在 put 之后进行 wait 操作
synchronized (this){
//在获取后时间未到就阻塞等待,等待设定时间与当前时间之差的时长
this.wait(myTask.getTime() - curTime);
}
}else{
//表明时间到了,可以执行任务
myTask.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
}
//指定两个参数
//第一个是指定任务
//第二个是指定在多少秒后执行
public void schedule(Runnable runnable,long after){
//这里要注意的是,after 是要在当前时间下,在等待多长时间,这里的时间戳获取当前的时间
MyTask myTask = new MyTask(runnable,System.currentTimeMillis() + after);
queue.put(myTask);
//在插入元素后就进行唤醒
synchronized (this){
this.notify();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务2");
}
},2000);
}
}